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

Conflicts:
	wagtail/wagtailimages/tests.py
This commit is contained in:
Tom Talbot 2014-07-25 14:41:26 +01:00
commit 6232e27029
36 changed files with 3147 additions and 114 deletions

View file

@ -3,12 +3,15 @@ Changelog
0.5 (xx.xx.20xx)
~~~~~~~~~~~~~~~~
* Added multiple image uploader
* Added RoutablePage model to allow embedding Django-style URL routing within a page
* Explorer nav now rendered separately and fetched with AJAX when needed
* Added decorator syntax for hooks
* Replaced lxml dependency with html5lib, to simplify installation
* Added page_unpublished signal
* Fix: Updates to tag fields are now properly committed to the database when publishing directly from the page edit interface
0.4.1 (14.07.2014)
~~~~~~~~~~~~~~~~~~
* ElasticSearch backend now respects the backward-compatible URLS configuration setting, in addition to HOSTS

View file

@ -10,6 +10,12 @@ Wagtail 0.5 release notes - IN DEVELOPMENT
What's new
==========
Multiple image uploader
~~~~~~~~~~~~~~~~~~~~~~~
The image uploader UI has been improved to allow multiples to be uploaded quickly.
RoutablePage
~~~~~~~~~~~~
@ -49,6 +55,8 @@ Admin
Bug fixes
~~~~~~~~~
* Updates to tag fields are now properly committed to the database when publishing directly from the page edit interface.
Backwards incompatible changes
==============================

View file

@ -24,6 +24,7 @@
<li><a href="#typography">Typography</a></li>
<li><a href="#help">Help text</a></li>
<li><a href="#listings">Listings</a></li>
<li><a href="#pagination">Pagination</a></li>
<li><a href="#buttons">Buttons</a></li>
<li><a href="#dropdowns">Dropdown buttons</a></li>
<li><a href="#header">Header</a></li>
@ -125,14 +126,21 @@
<tbody>
<tr>
<td class="title">
<h2>TD with title class</h2>
<h2><a href="">TD with title class</a></h2>
</td>
<td>Regular listing TD</td>
<td>Regular listing TD</td>
</tr>
<tr class="unpublished">
<td class="title">
<h2><a href="">Unpublished TD with title class</a></h2>
</td>
<td>Regular listing TD</td>
<td>Regular listing TD</td>
</tr>
<tr>
<td class="title">
<h2>TD with title class</h2>
<h2><a href="">TD with title class</a></h2>
</td>
<td>Regular listing TD</td>
<td>Regular listing TD</td>
@ -142,9 +150,41 @@
<h3><code>ul</code> listing</h3>
<ul class="listing">
<li>List item</li>
<li>List item</li>
<li>List item</li>
<li><div class="title"><h2><a href="">List item</a></h2></div></li>
<li><div class="title"><h2><a href="">List item</a></h2></div></li>
<li><div class="title"><h2><a href="">List item</a></h2></div></li>
</ul>
<h3><code>ul</code> listings with multiple columns</h3>
<ul class="listing">
<li>
<div class="row row-flush">
<div class="col6 title">
<h2><a href="">Something here</a></h2>
</div>
<small class="col6" style="text-align:right">Something else</small>
</div>
</li>
<li>
<div class="row row-flush">
<div class="col6">
<a href="">Something here</a>
</div>
<small class="col6" style="text-align:right">Something else</small>
</div>
</li>
<li>
<div class="row row-flush">
<div class="col6">
<a href="">Something here</a>
</div>
<small class="col6" style="text-align:right">Something else</small>
</div>
</li>
</ul>
<h3>Listings used for choosing a list item</h3>
@ -157,16 +197,23 @@
</tr>
</thead>
<tbody>
<tr class="can-choose">
<tr>
<td class="title">
<h2><a href="#">TD with title class</a></h2>
</td>
<td>Regular listing TD</td>
<td>Regular listing TD</td>
</li>
<tr class="can-choose">
<tr class="disabled">
<td class="title">
<h2>TD with title class</h2>
<h2>Disabled TD with title class</h2>
</td>
<td>Regular listing TD</td>
<td>Regular listing TD</td>
</li>
<tr >
<td class="title">
<h2><a href="">TD with title class</a></h2>
</td>
<td>Regular listing TD</td>
<td>Regular listing TD</td>
@ -175,6 +222,11 @@
</table>
</section>
<section id="pagination">
<h2>Pagination</h2>
{% include "wagtailadmin/shared/pagination_nav.html" with items=fake_pagination linkurl="wagtailadmin_explore" %}
</section>
<section id="buttons">
<h2>Buttons</h2>

View file

@ -32,7 +32,20 @@ def index(request):
messages.warning(request, _("Warning message"))
messages.error(request, _("Error message"))
fake_pagination = {
'number': 1,
'previous_page_number': 1,
'next_page_number': 2,
'has_previous': True,
'has_next': True,
'paginator': {
'num_pages': 10,
},
}
return render(request, 'wagtailstyleguide/base.html', {
'search_form': form,
'example_form': example_form,
'fake_pagination': fake_pagination,
})

View file

@ -4,7 +4,10 @@ from django.utils.encoding import python_2_unicode_compatible
from django.conf.urls import url
from django.http import HttpResponse
from taggit.models import TaggedItemBase
from modelcluster.fields import ParentalKey
from modelcluster.tags import ClusterTaggableManager
from wagtail.wagtailcore.models import Page, Orderable
from wagtail.wagtailcore.fields import RichTextField
@ -416,3 +419,11 @@ class RoutablePageTest(RoutablePage):
def main(self, request):
return HttpResponse("MAIN VIEW")
class TaggedPageTag(TaggedItemBase):
content_object = ParentalKey('tests.TaggedPage', related_name='tagged_items')
class TaggedPage(Page):
tags = ClusterTaggableManager(through=TaggedPageTag, blank=True)

View file

@ -101,6 +101,13 @@ $(function(){
}
});
/* Dropzones */
$('.drop-zone').on('dragover', function(){
$(this).addClass('hovered');
}).on('dragleave dragend drop', function(){
$(this).removeClass('hovered');
});
/* Header search behaviour */
if(window.headerSearch){
var search_current_index = 0;

View file

@ -98,7 +98,7 @@ function initTimeChooser(id) {
function initDateTimeChooser(id) {
if (window.dateTimePickerTranslations) {
$('#' + id).datetimepicker({
format: 'Y-m-d H:i',
format: 'Y-m-d H:i:s',
scrollInput:false,
i18n: {
lang: window.dateTimePickerTranslations
@ -106,8 +106,8 @@ function initDateTimeChooser(id) {
language: 'lang'
});
} else {
$('#' + id).datetimepicker({
format: 'Y-m-d H:i',
$('#' + id).datetimepicker({
format: 'Y-m-d H:i:s',
});
}
}

File diff suppressed because one or more lines are too long

View file

@ -121,8 +121,8 @@
color:lighten($color-grey-2,30%);
-webkit-font-smoothing: auto;
font-size:0.80em;
margin:0 0.5em 0.5em;
background:white url( "#{$static-root}bg-dark-diag.svg");
margin:0 0.5em;
background:white url("#{$static-root}bg-dark-diag.svg");
&.primary{
color:$color-grey-2;
@ -214,4 +214,13 @@ img{
/* utility class to allow things to be scrollable if their contents can't wrap more nicely */
.overflow{
overflow:auto;
}
}
.status-msg{
&.success{
color:$color-green;
}
&.failure{
color:$color-red;
}
}

View file

@ -172,7 +172,7 @@ input[type=checkbox]:before{
height:20px;
background-color:white;
border:1px solid $color-grey-4;
color:$color-grey-4;
color:white;
}
input[type=checkbox]:checked:before{
color:$color-teal;
@ -316,6 +316,7 @@ input[type=submit], input[type=reset], input[type=button], button{
> li{
@include row();
position:relative;
overflow:hidden;
background-color:white;
padding:1em 10em 1em 1.5em; /* 10em padding leaves room for controls */
margin-bottom:1em;
@ -726,6 +727,25 @@ ul.tagit li.tagit-choice-editable{
}
}
/* file drop zones */
.drop-zone{
@include border-radius(5px);
border:2px dashed $color-grey-4;
padding:$mobile-nice-padding;
background-color:$color-grey-5;
margin-bottom:1em;
text-align:center;
.drop-zone-help{
border:0;
}
&.hovered{
border-color:$color-teal;
background-color:$color-input-focus;
}
}
/* Transitions */
fieldset, input, textarea, select{
@include transition(background-color 0.2s ease);

View file

@ -5,6 +5,9 @@ ul.listing{
.listing{
margin-bottom:2em;
color:lighten($color-text-base, 20%);
font-size:0.95em;
ul{
@include unlist();
}
@ -27,7 +30,8 @@ ul.listing{
}
thead{
color: $color-grey-2;
font-size:1.1em;
color: $color-text-base;
border-bottom:1px solid $color-grey-4;
th{
@ -51,22 +55,6 @@ ul.listing{
right:0;
}
}
/*
.dropdown{
padding:0;
}
.dropdown-toggle{
display:block;
padding:1.5em;
}
.dropdown:hover,
.dropdown.open{
@include box-shadow(0px 0px 3px 0 rgba(0,0,0,0.2));
background-color:white;
margin-left:-$grid-gutter-width * 2;
}
*/
}
&.full-width td:first-child,
@ -87,32 +75,56 @@ ul.listing{
tbody{
border-bottom:1px dashed $color-input-border;
}
tbody tr{
border-top:1px dashed $color-input-border;
&:first-child{
tr{
border-top:1px dashed $color-input-border;
&:first-child{
border-top:1px dashed $color-input-border;
}
}
}
&.full-width tbody{
border:0;
}
&.full-width tbody tr:hover{
background-color:#FDFDFD;
}
&.chooser tr.can-choose a{
@include transition(none);
}
&.chooser tr.can-choose:hover{
background-color:$color-teal;
color:white;
a,a:hover{
color:white;
&.chooser {
tbody .title a{
display:block;
@include transition(none);
}
.status-tag{
border-color:white;
tbody tr:hover{
background-color:$color-teal;
color:white;
.title a, .title a:hover{
color:white;
}
.status-tag{
border-color:white;
}
}
tbody tr.disabled td{
opacity:0.25;
}
tbody tr.disabled td.children{
opacity:1;
}
tbody tr.disabled:hover{
background-color:inherit;
color:inherit;
.title{
cursor:not-allowed;
}
.status-tag{
border-color:inherit;
}
}
}
@ -120,22 +132,10 @@ ul.listing{
font-size:1em;
}
.divider{
text-transform:uppercase;
font-size:0.8em;
background-color:$color-grey-3;
td{
padding-top:0.5em;
padding-bottom:0.5em;
}
}
&.full-width .divider td{
padding-left:20px;
}
/* specific columns */
.bulk{
padding-right:0;
@ -159,9 +159,9 @@ ul.listing{
h2{
text-transform:none;
margin:0;
font-size:1.1em;
font-weight:700;
color:$color-grey-2;
font-size:1.15em;
font-weight:600;
color:darken($color-grey-2, 10%);
line-height:1.5em;
a{
@ -197,6 +197,7 @@ ul.listing{
color:$color-teal;
border-color:$color-grey-3;
background:white;
font-size:0.84em; /* 0.01em difference to regular small buttons */
&:hover{
border-color:$color-teal;
@ -213,27 +214,30 @@ ul.listing{
.children,
.no-children{
padding:0 !important;
border-left:1px dashed $color-input-border;
&:hover{
background-color:$color-grey-5;
}
}
.children a,
.no-children a{
display:block;
padding:2em 0;
a{
display:block;
padding:2em 0;
}
}
.children a{
color:$color-teal;
display:block;
&:before{
font-size:3rem;
}
}
.no-children a{
color:$color-grey-3;
display:block;
&:before{
font-size:1.5rem;
}
@ -241,6 +245,7 @@ ul.listing{
color:$color-teal;
}
}
&.small .children a:before{
font-size:30px;
}
@ -273,7 +278,6 @@ ul.listing{
border:1px dashed $color-input-border;
border-width:1px 0;
td{
display:none;
@ -297,20 +301,12 @@ ul.listing{
}
}
td.children:hover{
background-color:$color-teal;
a:before{
color:white;
}
}
table .no-results-message{
padding-left:20px;
}
.inactive h2{
opacity:0.5;
.unpublished h2{
opacity:0.7;
}
.index {
@ -319,6 +315,7 @@ ul.listing{
.title h2{
font-size:1.2em;
opacity:1;
a{
@include transition(opacity 0.2s ease);
}
@ -379,7 +376,7 @@ table.listing{
h2{
color:white;
font-size:1.8em;
font-weight:600;
font-weight:500;
a:hover{
color:white;
@ -526,15 +523,27 @@ table.listing{
}
.no-children {
border-color:transparent;
a{
opacity:0;
}
}
.no-children a{
opacity:0;
tr:hover .no-children a{
opacity:1;
}
.no-children:hover{
border-color:$color-input-border;
a{opacity:1;}
tr:hover .children{
background-color:$color-teal;
a:before{
color:white;
}
}
td.children:hover{
background-color:$color-teal-darker;
}
table .no-results-message{
padding-left:50px;
}

View file

@ -0,0 +1,25 @@
.progress{
@include border-radius(1.2em);
background-color:$color-teal-dark;
border:1px solid $color-teal;
opacity:0;
&.active{
opacity:1;
@include transition(opacity 0.3s ease);
}
.bar{
@include border-radius(1.5em);
@include transition(width 0.3s ease);
overflow:hidden;
box-sizing:border-box;
text-align:right;
line-height:1.2em;
color:white;
font-size:0.85em;
background-color:$color-teal;
height:1.2em;
padding-right:1em;
}
}

View file

@ -69,7 +69,6 @@ kbd{
}
.help-info, .help-warning, .help-critical{
@include border-radius(3px);
border:1px solid $color-grey-4;
padding-left:3.5em;
position:relative;
@ -83,7 +82,7 @@ kbd{
}
}
.help-info{
border-color:$color-blue;
background-color:lighten($color-blue, 30%);
&:before{
color:$color-blue;
@ -91,7 +90,7 @@ kbd{
}
.help-warning{
border-color:$color-orange;
background-color:lighten($color-orange, 30%);
&:before{
color:$color-orange;
@ -100,7 +99,7 @@ kbd{
}
.help-critical{
border-color:$color-red;
background-color:lighten($color-red, 40%);
&:before{
color:$color-red;

View file

@ -13,6 +13,7 @@
@import "components/messages.scss";
@import "components/formatters.scss";
@import "components/header.scss";
@import "components/progressbar.scss";
@import "components/datetimepicker.scss";
@import "fonts.scss";
@ -172,6 +173,7 @@ body{
margin:0;
position:absolute;
right:0.5em;
top:0.5em;
margin-top:0.15em;
}
}

View file

@ -23,12 +23,12 @@
{% endif %}
<th class="type">{% trans 'Type' %}</th>
<th class="status">{% trans 'Status' %}</th>
<th class="children"></th>
<th></th>
</tr>
{% endif %}
{% if parent_page %}
{% page_permissions parent_page as parent_page_perms %}
<tr class="index {% if not parent_page.live %} inactive{% endif %} {% if moving or choosing %}{% if parent_page.can_choose %}can-choose{% endif %}{% endif %}">
<tr class="index {% if not parent_page.live %} unpublished{% endif %} {% if moving or choosing %}{% if parent_page.can_choose %}can-disabled{% endif %}{% endif %}">
<td class="title" {% if orderable %}colspan="2"{% endif %}>
{% if moving %}
<h2>
@ -145,7 +145,7 @@
{% trans 'Status' %}
{% endif %}
</th>
<th class="children"></th>
<th></th>
</tr>
{% endif %}
</thead>
@ -153,7 +153,7 @@
{% if pages %}
{% for page in pages %}
{% page_permissions page as page_perms %}
<tr {% if ordering == "ord" %}id="page_{{ page.id }}" data-page-title="{{ page.title }}"{% endif %} class="{% if not page.live %} inactive{% endif %}{% if moving and page.can_choose %} can-choose{% elif choosing and page.can_choose %} can-choose{% else %} cant-choose{% endif %}">
<tr {% if ordering == "ord" %}id="page_{{ page.id }}" data-page-title="{{ page.title }}"{% endif %} class="{% if not page.live %} unpublished{% endif %}{% if moving or choosing %}{% if not page.can_choose %}disabled{% endif %}{% endif %}">
{% if orderable %}
<td class="ord">{% if ordering == "ord" %}<div class="handle icon icon-grip text-replace">{% trans 'Drag' %}</div>{% endif %}</td>
{% endif %}
@ -239,7 +239,7 @@
{% endif %}
{% else %}
{% if page.is_navigable %}
<a href="{% url 'wagtailadmin_explore' page.id %}" class="icon text-replace icon-arrow-right" title="{% blocktrans with title=page.title %}Explorer child pages of '{{ title }}'{% endblocktrans %}">{% trans "Explore" %}</a>
<a href="{% url 'wagtailadmin_explore' page.id %}" class="icon text-replace icon-arrow-right" title="{% blocktrans with title=page.title %}Explore child pages of '{{ title }}'{% endblocktrans %}">{% trans "Explore" %}</a>
{% elif page_perms.can_add_subpage %}
<a href="{% url 'wagtailadmin_pages_add_subpage' page.id %}" class="icon text-replace icon-plus-inverse" title="{% blocktrans with title=page.title %}Add a child page to '{{ title }}'{% endblocktrans %}">{% trans 'Add subpage' %}</a>
{% endif %}

View file

@ -7,7 +7,7 @@ from django.core import mail
from django.core.paginator import Paginator
from django.utils import timezone
from wagtail.tests.models import SimplePage, EventPage, EventPageCarouselItem, StandardIndex, BusinessIndex, BusinessChild, BusinessSubIndex
from wagtail.tests.models import SimplePage, EventPage, EventPageCarouselItem, StandardIndex, BusinessIndex, BusinessChild, BusinessSubIndex, TaggedPage
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.wagtailcore.models import Page, PageRevision
from wagtail.wagtailcore.signals import page_published, page_unpublished
@ -1410,3 +1410,36 @@ class TestNotificationPreferences(TestCase, WagtailTestUtils):
# No email to send
self.assertEqual(len(mail.outbox), 0)
class TestIssue197(TestCase, WagtailTestUtils):
def test_issue_197(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Create a tagged page with no tags
self.tagged_page = self.root_page.add_child(instance=TaggedPage(
title="Tagged page",
slug='tagged-page',
live=False,
))
# Login
self.user = self.login()
# Add some tags and publish using edit view
post_data = {
'title': "Tagged page",
'slug':'tagged-page',
'tags': "hello, world",
'action-publish': "Publish",
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.tagged_page.id, )), post_data)
# Should be redirected to explorer page
self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Check that both tags are in the pages tag set
page = TaggedPage.objects.get(id=self.tagged_page.id)
self.assertIn('hello', page.tags.slugs())
self.assertIn('world', page.tags.slugs())

View file

@ -313,7 +313,13 @@ def edit(request, page_id):
approved_go_live_at = go_live_at
else:
page.live = True
form.save()
# We need save the page this way to workaround a bug
# in django-modelcluster causing m2m fields to not
# be committed to the database. See github issue #192
form.save(commit=False)
page.save()
# Clear approved_go_live_at for older revisions
page.revisions.update(
submitted_for_moderation=False,
@ -328,7 +334,9 @@ def edit(request, page_id):
Page.objects.filter(id=page.id).update(has_unpublished_changes=True)
else:
page.has_unpublished_changes = True
form.save()
form.save(commit=False)
page.save()
page.save_revision(
user=request.user,

View file

@ -286,8 +286,8 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, indexed.Index
show_in_menus = models.BooleanField(default=False, help_text=_("Whether a link to this page will appear in automatically generated menus"))
search_description = models.TextField(blank=True)
go_live_at = models.DateTimeField(verbose_name=_("Go live date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm."), blank=True, null=True)
expire_at = models.DateTimeField(verbose_name=_("Expiry date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm."), blank=True, null=True)
go_live_at = models.DateTimeField(verbose_name=_("Go live date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm:ss."), blank=True, null=True)
expire_at = models.DateTimeField(verbose_name=_("Expiry date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm:ss."), blank=True, null=True)
expired = models.BooleanField(default=False, editable=False)
search_fields = (

View file

@ -14,6 +14,11 @@ def get_image_form():
widgets={'file': forms.FileInput()})
def get_image_form_for_multi():
# exclude the file widget
return modelform_factory(get_image_model(), exclude=('file',))
class ImageInsertionForm(forms.Form):
"""
Form for selecting parameters of the image (e.g. format) prior to insertion

View file

@ -0,0 +1,155 @@
$(function(){
// Redirect users that don't support filereader
if(!$('html').hasClass('filereader')){
document.location.href = window.simple_upload_url;
return false;
}
// prevents browser default drag/drop
$(document).bind('drop dragover', function (e) {
e.preventDefault();
});
$('#fileupload').fileupload({
dataType: 'html',
sequentialUploads: true,
dropZone: $('.drop-zone'),
acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i,
previewMinWidth:150,
previewMaxWidth:150,
previewMinHeight:150,
previewMaxHeight:150,
add: function (e, data) {
var $this = $(this);
var that = $this.data('blueimp-fileupload') || $this.data('fileupload')
var li = $($('#upload-list-item').html()).addClass('upload-uploading')
var options = that.options;
$('#upload-list').append(li);
data.context = li;
data.process(function () {
return $this.fileupload('process', data);
}).always(function () {
data.context.removeClass('processing');
data.context.find('.left').each(function(index, elm){
$(elm).append(data.files[index].name);
});
data.context.find('.preview .thumb').each(function (index, elm) {
$(elm).addClass('hasthumb')
$(elm).append(data.files[index].preview);
});
}).done(function () {
data.context.find('.start').prop('disabled', false);
if ((that._trigger('added', e, data) !== false) &&
(options.autoUpload || data.autoUpload) &&
data.autoUpload !== false) {
data.submit();
}
}).fail(function () {
if (data.files.error) {
data.context.each(function (index) {
var error = data.files[index].error;
if (error) {
$(this).find('.error').text(error);
}
});
}
});
},
progress: function (e, data) {
if (e.isDefaultPrevented()) {
return false;
}
var progress = Math.floor(data.loaded / data.total * 100);
data.context.each(function () {
$(this).find('.progress').addClass('active').attr('aria-valuenow', progress).find('.bar').css(
'width',
progress + '%'
).html(progress + '%');
});
},
progressall: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
$('#overall-progress').addClass('active').attr('aria-valuenow', progress).find('.bar').css(
'width',
progress + '%'
).html(progress + '%');
if (progress >= 100){
$('#overall-progress').removeClass('active').find('.bar').css('width','0%');
}
},
done: function (e, data) {
var itemElement = $(data.context);
var response = $.parseJSON(data.result);
if(response.success){
itemElement.addClass('upload-success')
$('.right', itemElement).append(response.form);
// run tagit enhancement
$('.tag_field input', itemElement).tagit(window.tagit_opts);
} else {
itemElement.addClass('upload-failure');
$('.right .error_messages', itemElement).append(response.error_message);
}
},
fail: function(e, data){
var itemElement = $(data.context);
itemElement.addClass('upload-failure');
},
always: function(e, data){
var itemElement = $(data.context);
itemElement.removeClass('upload-uploading').addClass('upload-complete');
},
});
// ajax-enhance forms added on done()
$('#upload-list').on('submit', 'form', function(e){
var form = $(this);
var itemElement = form.closest('#upload-list > li');
console.log(form);
e.preventDefault();
$.post(this.action, form.serialize(), function(data) {
if (data.success) {
itemElement.slideUp(function(){$(this).remove()});
}else{
form.replaceWith(data.form);
// run tagit enhancement on new form
$('.tag_field input', form).tagit(window.tagit_opts);
}
});
});
$('#upload-list').on('click', '.delete', function(e){
var form = $(this).closest('form');
var itemElement = form.closest('#upload-list > li');
e.preventDefault();
var CSRFToken = $('input[name="csrfmiddlewaretoken"]', form).val();
$.post(this.href, {csrfmiddlewaretoken: CSRFToken}, function(data) {
if (data.success) {
itemElement.slideUp(function(){$(this).remove()});
}else{
}
});
});
});

View file

@ -0,0 +1 @@
!function(a){"use strict";var b=a.HTMLCanvasElement&&a.HTMLCanvasElement.prototype,c=a.Blob&&function(){try{return Boolean(new Blob)}catch(a){return!1}}(),d=c&&a.Uint8Array&&function(){try{return 100===new Blob([new Uint8Array(100)]).size}catch(a){return!1}}(),e=a.BlobBuilder||a.WebKitBlobBuilder||a.MozBlobBuilder||a.MSBlobBuilder,f=(c||e)&&a.atob&&a.ArrayBuffer&&a.Uint8Array&&function(a){var b,f,g,h,i,j;for(b=a.split(",")[0].indexOf("base64")>=0?atob(a.split(",")[1]):decodeURIComponent(a.split(",")[1]),f=new ArrayBuffer(b.length),g=new Uint8Array(f),h=0;h<b.length;h+=1)g[h]=b.charCodeAt(h);return i=a.split(",")[0].split(":")[1].split(";")[0],c?new Blob([d?g:f],{type:i}):(j=new e,j.append(f),j.getBlob(i))};a.HTMLCanvasElement&&!b.toBlob&&(b.mozGetAsFile?b.toBlob=function(a,c,d){d&&b.toDataURL&&f?a(f(this.toDataURL(c,d))):a(this.mozGetAsFile("blob",c))}:b.toDataURL&&f&&(b.toBlob=function(a,b,c){a(f(this.toDataURL(b,c)))})),"function"==typeof define&&define.amd?define(function(){return f}):a.dataURLtoBlob=f}(this);

View file

@ -0,0 +1,315 @@
/*
* jQuery File Upload Image Preview & Resize Plugin 1.7.2
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2013, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
/* jshint nomen:false */
/* global define, window, Blob */
(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module:
define([
'jquery',
'load-image',
'load-image-meta',
'load-image-exif',
'load-image-ios',
'canvas-to-blob',
'./jquery.fileupload-process'
], factory);
} else {
// Browser globals:
factory(
window.jQuery,
window.loadImage
);
}
}(function ($, loadImage) {
'use strict';
// Prepend to the default processQueue:
$.blueimp.fileupload.prototype.options.processQueue.unshift(
{
action: 'loadImageMetaData',
disableImageHead: '@',
disableExif: '@',
disableExifThumbnail: '@',
disableExifSub: '@',
disableExifGps: '@',
disabled: '@disableImageMetaDataLoad'
},
{
action: 'loadImage',
// Use the action as prefix for the "@" options:
prefix: true,
fileTypes: '@',
maxFileSize: '@',
noRevoke: '@',
disabled: '@disableImageLoad'
},
{
action: 'resizeImage',
// Use "image" as prefix for the "@" options:
prefix: 'image',
maxWidth: '@',
maxHeight: '@',
minWidth: '@',
minHeight: '@',
crop: '@',
orientation: '@',
forceResize: '@',
disabled: '@disableImageResize'
},
{
action: 'saveImage',
quality: '@imageQuality',
type: '@imageType',
disabled: '@disableImageResize'
},
{
action: 'saveImageMetaData',
disabled: '@disableImageMetaDataSave'
},
{
action: 'resizeImage',
// Use "preview" as prefix for the "@" options:
prefix: 'preview',
maxWidth: '@',
maxHeight: '@',
minWidth: '@',
minHeight: '@',
crop: '@',
orientation: '@',
thumbnail: '@',
canvas: '@',
disabled: '@disableImagePreview'
},
{
action: 'setImage',
name: '@imagePreviewName',
disabled: '@disableImagePreview'
},
{
action: 'deleteImageReferences',
disabled: '@disableImageReferencesDeletion'
}
);
// The File Upload Resize plugin extends the fileupload widget
// with image resize functionality:
$.widget('blueimp.fileupload', $.blueimp.fileupload, {
options: {
// The regular expression for the types of images to load:
// matched against the file type:
loadImageFileTypes: /^image\/(gif|jpeg|png|svg\+xml)$/,
// The maximum file size of images to load:
loadImageMaxFileSize: 10000000, // 10MB
// The maximum width of resized images:
imageMaxWidth: 1920,
// The maximum height of resized images:
imageMaxHeight: 1080,
// Defines the image orientation (1-8) or takes the orientation
// value from Exif data if set to true:
imageOrientation: false,
// Define if resized images should be cropped or only scaled:
imageCrop: false,
// Disable the resize image functionality by default:
disableImageResize: true,
// The maximum width of the preview images:
previewMaxWidth: 80,
// The maximum height of the preview images:
previewMaxHeight: 80,
// Defines the preview orientation (1-8) or takes the orientation
// value from Exif data if set to true:
previewOrientation: true,
// Create the preview using the Exif data thumbnail:
previewThumbnail: true,
// Define if preview images should be cropped or only scaled:
previewCrop: false,
// Define if preview images should be resized as canvas elements:
previewCanvas: true
},
processActions: {
// Loads the image given via data.files and data.index
// as img element, if the browser supports the File API.
// Accepts the options fileTypes (regular expression)
// and maxFileSize (integer) to limit the files to load:
loadImage: function (data, options) {
if (options.disabled) {
return data;
}
var that = this,
file = data.files[data.index],
dfd = $.Deferred();
if (($.type(options.maxFileSize) === 'number' &&
file.size > options.maxFileSize) ||
(options.fileTypes &&
!options.fileTypes.test(file.type)) ||
!loadImage(
file,
function (img) {
if (img.src) {
data.img = img;
}
dfd.resolveWith(that, [data]);
},
options
)) {
return data;
}
return dfd.promise();
},
// Resizes the image given as data.canvas or data.img
// and updates data.canvas or data.img with the resized image.
// Also stores the resized image as preview property.
// Accepts the options maxWidth, maxHeight, minWidth,
// minHeight, canvas and crop:
resizeImage: function (data, options) {
if (options.disabled || !(data.canvas || data.img)) {
return data;
}
options = $.extend({canvas: true}, options);
var that = this,
dfd = $.Deferred(),
img = (options.canvas && data.canvas) || data.img,
resolve = function (newImg) {
if (newImg && (newImg.width !== img.width ||
newImg.height !== img.height ||
options.forceResize)) {
data[newImg.getContext ? 'canvas' : 'img'] = newImg;
}
data.preview = newImg;
dfd.resolveWith(that, [data]);
},
thumbnail;
if (data.exif) {
if (options.orientation === true) {
options.orientation = data.exif.get('Orientation');
}
if (options.thumbnail) {
thumbnail = data.exif.get('Thumbnail');
if (thumbnail) {
loadImage(thumbnail, resolve, options);
return dfd.promise();
}
}
// Prevent orienting the same image twice:
if (data.orientation) {
delete options.orientation;
} else {
data.orientation = options.orientation;
}
}
if (img) {
resolve(loadImage.scale(img, options));
return dfd.promise();
}
return data;
},
// Saves the processed image given as data.canvas
// inplace at data.index of data.files:
saveImage: function (data, options) {
if (!data.canvas || options.disabled) {
return data;
}
var that = this,
file = data.files[data.index],
dfd = $.Deferred();
if (data.canvas.toBlob) {
data.canvas.toBlob(
function (blob) {
if (!blob.name) {
if (file.type === blob.type) {
blob.name = file.name;
} else if (file.name) {
blob.name = file.name.replace(
/\..+$/,
'.' + blob.type.substr(6)
);
}
}
// Don't restore invalid meta data:
if (file.type !== blob.type) {
delete data.imageHead;
}
// Store the created blob at the position
// of the original file in the files list:
data.files[data.index] = blob;
dfd.resolveWith(that, [data]);
},
options.type || file.type,
options.quality
);
} else {
return data;
}
return dfd.promise();
},
loadImageMetaData: function (data, options) {
if (options.disabled) {
return data;
}
var that = this,
dfd = $.Deferred();
loadImage.parseMetaData(data.files[data.index], function (result) {
$.extend(data, result);
dfd.resolveWith(that, [data]);
}, options);
return dfd.promise();
},
saveImageMetaData: function (data, options) {
if (!(data.imageHead && data.canvas &&
data.canvas.toBlob && !options.disabled)) {
return data;
}
var file = data.files[data.index],
blob = new Blob([
data.imageHead,
// Resized images always have a head size of 20 bytes,
// including the JPEG marker and a minimal JFIF header:
this._blobSlice.call(file, 20)
], {type: file.type});
blob.name = file.name;
data.files[data.index] = blob;
return data;
},
// Sets the resized version of the image as a property of the
// file object, must be called after "saveImage":
setImage: function (data, options) {
if (data.preview && !options.disabled) {
data.files[data.index][options.name || 'preview'] = data.preview;
}
return data;
},
deleteImageReferences: function (data, options) {
if (!options.disabled) {
delete data.img;
delete data.canvas;
delete data.preview;
delete data.imageHead;
}
return data;
}
}
});
}));

View file

@ -0,0 +1,172 @@
/*
* jQuery File Upload Processing Plugin 1.3.0
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2012, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
/* jshint nomen:false */
/* global define, window */
(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module:
define([
'jquery',
'./jquery.fileupload'
], factory);
} else {
// Browser globals:
factory(
window.jQuery
);
}
}(function ($) {
'use strict';
var originalAdd = $.blueimp.fileupload.prototype.options.add;
// The File Upload Processing plugin extends the fileupload widget
// with file processing functionality:
$.widget('blueimp.fileupload', $.blueimp.fileupload, {
options: {
// The list of processing actions:
processQueue: [
/*
{
action: 'log',
type: 'debug'
}
*/
],
add: function (e, data) {
var $this = $(this);
data.process(function () {
return $this.fileupload('process', data);
});
originalAdd.call(this, e, data);
}
},
processActions: {
/*
log: function (data, options) {
console[options.type](
'Processing "' + data.files[data.index].name + '"'
);
}
*/
},
_processFile: function (data, originalData) {
var that = this,
dfd = $.Deferred().resolveWith(that, [data]),
chain = dfd.promise();
this._trigger('process', null, data);
$.each(data.processQueue, function (i, settings) {
var func = function (data) {
if (originalData.errorThrown) {
return $.Deferred()
.rejectWith(that, [originalData]).promise();
}
return that.processActions[settings.action].call(
that,
data,
settings
);
};
chain = chain.pipe(func, settings.always && func);
});
chain
.done(function () {
that._trigger('processdone', null, data);
that._trigger('processalways', null, data);
})
.fail(function () {
that._trigger('processfail', null, data);
that._trigger('processalways', null, data);
});
return chain;
},
// Replaces the settings of each processQueue item that
// are strings starting with an "@", using the remaining
// substring as key for the option map,
// e.g. "@autoUpload" is replaced with options.autoUpload:
_transformProcessQueue: function (options) {
var processQueue = [];
$.each(options.processQueue, function () {
var settings = {},
action = this.action,
prefix = this.prefix === true ? action : this.prefix;
$.each(this, function (key, value) {
if ($.type(value) === 'string' &&
value.charAt(0) === '@') {
settings[key] = options[
value.slice(1) || (prefix ? prefix +
key.charAt(0).toUpperCase() + key.slice(1) : key)
];
} else {
settings[key] = value;
}
});
processQueue.push(settings);
});
options.processQueue = processQueue;
},
// Returns the number of files currently in the processsing queue:
processing: function () {
return this._processing;
},
// Processes the files given as files property of the data parameter,
// returns a Promise object that allows to bind callbacks:
process: function (data) {
var that = this,
options = $.extend({}, this.options, data);
if (options.processQueue && options.processQueue.length) {
this._transformProcessQueue(options);
if (this._processing === 0) {
this._trigger('processstart');
}
$.each(data.files, function (index) {
var opts = index ? $.extend({}, options) : options,
func = function () {
if (data.errorThrown) {
return $.Deferred()
.rejectWith(that, [data]).promise();
}
return that._processFile(opts, data);
};
opts.index = index;
that._processing += 1;
that._processingQueue = that._processingQueue.pipe(func, func)
.always(function () {
that._processing -= 1;
if (that._processing === 0) {
that._trigger('processstop');
}
});
});
}
return this._processingQueue;
},
_create: function () {
this._super();
this._processing = 0;
this._processingQueue = $.Deferred().resolveWith(this)
.promise();
}
});
}));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,214 @@
/*
* jQuery Iframe Transport Plugin 1.8.2
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2011, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
/* global define, window, document */
(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module:
define(['jquery'], factory);
} else {
// Browser globals:
factory(window.jQuery);
}
}(function ($) {
'use strict';
// Helper variable to create unique names for the transport iframes:
var counter = 0;
// The iframe transport accepts four additional options:
// options.fileInput: a jQuery collection of file input fields
// options.paramName: the parameter name for the file form data,
// overrides the name property of the file input field(s),
// can be a string or an array of strings.
// options.formData: an array of objects with name and value properties,
// equivalent to the return data of .serializeArray(), e.g.:
// [{name: 'a', value: 1}, {name: 'b', value: 2}]
// options.initialIframeSrc: the URL of the initial iframe src,
// by default set to "javascript:false;"
$.ajaxTransport('iframe', function (options) {
if (options.async) {
// javascript:false as initial iframe src
// prevents warning popups on HTTPS in IE6:
/*jshint scripturl: true */
var initialIframeSrc = options.initialIframeSrc || 'javascript:false;',
/*jshint scripturl: false */
form,
iframe,
addParamChar;
return {
send: function (_, completeCallback) {
form = $('<form style="display:none;"></form>');
form.attr('accept-charset', options.formAcceptCharset);
addParamChar = /\?/.test(options.url) ? '&' : '?';
// XDomainRequest only supports GET and POST:
if (options.type === 'DELETE') {
options.url = options.url + addParamChar + '_method=DELETE';
options.type = 'POST';
} else if (options.type === 'PUT') {
options.url = options.url + addParamChar + '_method=PUT';
options.type = 'POST';
} else if (options.type === 'PATCH') {
options.url = options.url + addParamChar + '_method=PATCH';
options.type = 'POST';
}
// IE versions below IE8 cannot set the name property of
// elements that have already been added to the DOM,
// so we set the name along with the iframe HTML markup:
counter += 1;
iframe = $(
'<iframe src="' + initialIframeSrc +
'" name="iframe-transport-' + counter + '"></iframe>'
).bind('load', function () {
var fileInputClones,
paramNames = $.isArray(options.paramName) ?
options.paramName : [options.paramName];
iframe
.unbind('load')
.bind('load', function () {
var response;
// Wrap in a try/catch block to catch exceptions thrown
// when trying to access cross-domain iframe contents:
try {
response = iframe.contents();
// Google Chrome and Firefox do not throw an
// exception when calling iframe.contents() on
// cross-domain requests, so we unify the response:
if (!response.length || !response[0].firstChild) {
throw new Error();
}
} catch (e) {
response = undefined;
}
// The complete callback returns the
// iframe content document as response object:
completeCallback(
200,
'success',
{'iframe': response}
);
// Fix for IE endless progress bar activity bug
// (happens on form submits to iframe targets):
$('<iframe src="' + initialIframeSrc + '"></iframe>')
.appendTo(form);
window.setTimeout(function () {
// Removing the form in a setTimeout call
// allows Chrome's developer tools to display
// the response result
form.remove();
}, 0);
});
form
.prop('target', iframe.prop('name'))
.prop('action', options.url)
.prop('method', options.type);
if (options.formData) {
$.each(options.formData, function (index, field) {
$('<input type="hidden"/>')
.prop('name', field.name)
.val(field.value)
.appendTo(form);
});
}
if (options.fileInput && options.fileInput.length &&
options.type === 'POST') {
fileInputClones = options.fileInput.clone();
// Insert a clone for each file input field:
options.fileInput.after(function (index) {
return fileInputClones[index];
});
if (options.paramName) {
options.fileInput.each(function (index) {
$(this).prop(
'name',
paramNames[index] || options.paramName
);
});
}
// Appending the file input fields to the hidden form
// removes them from their original location:
form
.append(options.fileInput)
.prop('enctype', 'multipart/form-data')
// enctype must be set as encoding for IE:
.prop('encoding', 'multipart/form-data');
// Remove the HTML5 form attribute from the input(s):
options.fileInput.removeAttr('form');
}
form.submit();
// Insert the file input fields at their original location
// by replacing the clones with the originals:
if (fileInputClones && fileInputClones.length) {
options.fileInput.each(function (index, input) {
var clone = $(fileInputClones[index]);
// Restore the original name and form properties:
$(input)
.prop('name', clone.prop('name'))
.attr('form', clone.attr('form'));
clone.replaceWith(input);
});
}
});
form.append(iframe).appendTo(document.body);
},
abort: function () {
if (iframe) {
// javascript:false as iframe src aborts the request
// and prevents warning popups on HTTPS in IE6.
// concat is used to avoid the "Script URL" JSLint error:
iframe
.unbind('load')
.prop('src', initialIframeSrc);
}
if (form) {
form.remove();
}
}
};
}
});
// The iframe transport returns the iframe content document as response.
// The following adds converters from iframe to text, json, html, xml
// and script.
// Please note that the Content-Type for JSON responses has to be text/plain
// or text/html, if the browser doesn't include application/json in the
// Accept header, else IE will show a download dialog.
// The Content-Type for XML responses on the other hand has to be always
// application/xml or text/xml, so IE properly parses the XML response.
// See also
// https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation
$.ajaxSetup({
converters: {
'iframe text': function (iframe) {
return iframe && $(iframe[0].body).text();
},
'iframe json': function (iframe) {
return iframe && $.parseJSON($(iframe[0].body).text());
},
'iframe html': function (iframe) {
return iframe && $(iframe[0].body).html();
},
'iframe xml': function (iframe) {
var xmlDoc = iframe && iframe[0];
return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc :
$.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) ||
$(xmlDoc.body).html());
},
'iframe script': function (iframe) {
return iframe && $.globalEval($(iframe[0].body).text());
}
}
});
}));

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,120 @@
@import "../../wagtailadmin/static/wagtailadmin/scss/variables.scss";
@import "../../wagtailadmin/static/wagtailadmin/scss/mixins.scss";
@import "../../wagtailadmin/static/wagtailadmin/scss/grid.scss";
.replace-file-input{
display:inline-block;
position:relative;
overflow:hidden;
padding-bottom:2px;
input[type=file]{
padding:0;
opacity:0;
position:absolute;
top:0;
right:0;
direction:ltr;
width:auto;
display:block;
font-size:5em;
&:hover{
cursor:pointer;
}
}
&:hover{
cursor:pointer;
button{
background-color:$color-teal-darker;
}
}
}
.upload-list{
> li{
padding:1em;
}
.left{
text-align:center;
}
.preview{
width:150px;
min-height:150px;
display:block;
position:relative;
text-align:center;
max-width:100%;
margin:auto;
}
.progress, .thumb, .thumb:before, canvas, img{
position:absolute;
max-width:100%;
}
.progress{
z-index:4;
top:60%;
left:20%;
right:20%;
width:60%;
@include box-shadow(0 0 5px 2px rgba(255, 255, 255, 0.4));
}
.thumb{
top:0;right:0;bottom:0;left:0;
z-index:1;
width:100%;
}
.thumb:before, canvas, img{
left:0;
right:0;
top:0;
bottom:0;
margin:auto;
}
.thumb:before{
z-index:2;
top:0;
width:1em;
font-size:10em;
line-height:1.4em;
color:lighten($color-grey-4, 4%);
}
canvas, img{
z-index:3;
}
.hasthumb{
&:before{
display:none;
}
}
.status-msg{
display:none;
}
.upload-complete{
.progress{
opacity:0;
}
}
.upload-uploading{
}
.upload-success{
.status-msg.success{
display:block;
}
}
.upload-failure{
border-color:$color-red;
.status-msg.failure{
display:block;
}
}
}

View file

@ -17,7 +17,7 @@
{% block content %}
{% trans "Images" as im_str %}
{% trans "Add an image" as add_img_str %}
{% include "wagtailadmin/shared/header.html" with title=im_str add_link="wagtailimages_add_image" icon="image" add_text=add_img_str search_url="wagtailimages_index" %}
{% include "wagtailadmin/shared/header.html" with title=im_str add_link="wagtailimages_add_multiple" icon="image" add_text=add_img_str search_url="wagtailimages_index" %}
<div class="nice-padding">
<div id="image-results">

View file

@ -0,0 +1,77 @@
{% extends "wagtailadmin/base.html" %}
{% load image_tags i18n compress static %}
{% block titletag %}{% trans "Add multiple images" %}{% endblock %}
{% block bodyclass %}menu-images{% endblock %}
{% block extra_css %}
{% compress css %}
<link rel="stylesheet" href="{{ STATIC_URL }}wagtailimages/scss/add-multiple.scss" type="text/x-scss" />
{% endcompress %}
{% include "wagtailadmin/shared/tag_field_css.html" %}
{% endblock %}
{% block content %}
{% trans "Add images" as add_str %}
{% include "wagtailadmin/shared/header.html" with title=add_str icon="image" %}
<div class="nice-padding">
<div class="drop-zone">
<p>{% trans "Drag and drop images into this area to upload immediately." %}</p>
<form action="{% url 'wagtailimages_add_multiple' %}" method="POST" enctype="multipart/form-data">
<div class="replace-file-input">
<button class="bicolor icon icon-plus">{% trans "Or choose from your computer" %}</button>
<input id="fileupload" type="file" name="files[]" data-url="{% url 'wagtailimages_add_multiple' %}" multiple>
</div>
{% csrf_token %}
</form>
</div>
<div id="overall-progress" class="progress progress-secondary">
<div class="bar" style="width: 0%;">0%</div>
</div>
<ul id="upload-list" class="upload-list multiple"></ul>
</div>
<script id="upload-list-item" type="text/template">
<li class="row">
<div class="left col3">
<div class="preview">
<div class="thumb icon icon-image"></div>
<div class="progress">
<div class="bar" style="width: 0%;"></div>
</div>
</div>
</div>
<div class="right col9">
<p class="status-msg success">{% trans "Upload successful. Please update this image with a more appropriate title, if necessary. You may also delete the image completely if the upload wasn't required." %}</p>
<p class="status-msg failure">{% trans "Sorry, upload failed." %}</p>
<p class="status-msg failure error_messages"></p>
</div>
</li>
</script>
{% endblock %}
{% block extra_js %}
{% compress js %}
<!-- this exact order of plugins is vital -->
<script src="{{ STATIC_URL }}wagtailimages/js/vendor/load-image.min.js"></script>
<script src="{{ STATIC_URL }}wagtailimages/js/vendor/canvas-to-blob.min.js"></script>
<script src="{{ STATIC_URL }}wagtailimages/js/vendor/jquery.iframe-transport.js"></script>
<script src="{{ STATIC_URL }}wagtailimages/js/vendor/jquery.fileupload.js"></script>
<script src="{{ STATIC_URL }}wagtailimages/js/vendor/jquery.fileupload-process.js"></script>
<script src="{{ STATIC_URL }}wagtailimages/js/vendor/jquery.fileupload-image.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/tag-it.js"></script>
<!-- Main script -->
<script src="{{ STATIC_URL }}wagtailimages/js/add-multiple.js"></script>
{% endcompress %}
{% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %}
<script>
window.simple_upload_url = "{% url 'wagtailimages_add_image' %}";
window.tagit_opts = {
autocomplete: {source: "{{ autocomplete_url|addslashes }}"}
};
</script>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% load i18n %}
<form action="{% url 'wagtailimages_edit_multiple' image.id %}" method="POST" enctype="multipart/form-data">
<ul class="fields">
{% csrf_token %}
{% for field in form %}
{% include "wagtailadmin/shared/field_as_li.html" %}
{% endfor %}
<li>
<input type="submit" value="{% trans 'Update' %}" />
<a href="{% url 'wagtailimages_delete_multiple' image.id %}" class="delete button button-secondary no">{% trans "Delete" %}</a>
</li>
</ul>
</form>

View file

@ -1,3 +1,5 @@
import json
from mock import MagicMock
from django.utils import six
@ -523,3 +525,209 @@ class TestUsedBy(TestCase):
event_page_carousel_item.image = self.image
event_page_carousel_item.save()
self.assertTrue(issubclass(Page, type(self.image.used_by[0])))
class TestMultipleImageUploader(TestCase, WagtailTestUtils):
"""
This tests the multiple image upload views located in wagtailimages/views/multiple.py
"""
def setUp(self):
self.login()
# Create an image for running tests on
self.image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
def test_add(self):
"""
This tests that the add view responds correctly on a GET request
"""
# Send request
response = self.client.get(reverse('wagtailimages_add_multiple'))
# Check response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailimages/multiple/add.html')
def test_add_post(self):
"""
This tests that a POST request to the add view saves the image and returns an edit form
"""
response = self.client.post(reverse('wagtailimages_add_multiple'), {
'files[]': SimpleUploadedFile('test.png', get_test_image_file().file.getvalue()),
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Check response
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
self.assertTemplateUsed(response, 'wagtailimages/multiple/edit_form.html')
# Check image
self.assertIn('image', response.context)
self.assertEqual(response.context['image'].title, 'test.png')
# Check form
self.assertIn('form', response.context)
self.assertEqual(response.context['form'].initial['title'], 'test.png')
# Check JSON
response_json = json.loads(response.content.decode())
self.assertIn('image_id', response_json)
self.assertIn('form', response_json)
self.assertIn('success', response_json)
self.assertEqual(response_json['image_id'], response.context['image'].id)
self.assertTrue(response_json['success'])
def test_add_post_noajax(self):
"""
This tests that only AJAX requests are allowed to POST to the add view
"""
response = self.client.post(reverse('wagtailimages_add_multiple'), {})
# Check response
self.assertEqual(response.status_code, 400)
def test_add_post_nofile(self):
"""
This tests that the add view checks for a file when a user POSTs to it
"""
response = self.client.post(reverse('wagtailimages_add_multiple'), {}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Check response
self.assertEqual(response.status_code, 400)
def test_add_post_badfile(self):
"""
This tests that the add view checks for a file when a user POSTs to it
"""
response = self.client.post(reverse('wagtailimages_add_multiple'), {
'files[]': SimpleUploadedFile('test.png', b"This is not an image!"),
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Check response
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
# Check JSON
response_json = json.loads(response.content.decode())
self.assertNotIn('image_id', response_json)
self.assertNotIn('form', response_json)
self.assertIn('success', response_json)
self.assertIn('error_message', response_json)
self.assertFalse(response_json['success'])
self.assertEqual(response_json['error_message'], 'Not a valid image. Please use a gif, jpeg or png file with the correct file extension (*.gif, *.jpg or *.png).')
def test_edit_get(self):
"""
This tests that a GET request to the edit view returns a 405 "METHOD NOT ALLOWED" response
"""
# Send request
response = self.client.get(reverse('wagtailimages_edit_multiple', args=(self.image.id, )))
# Check response
self.assertEqual(response.status_code, 405)
def test_edit_post(self):
"""
This tests that a POST request to the edit view edits the image
"""
# Send request
response = self.client.post(reverse('wagtailimages_edit_multiple', args=(self.image.id, )), {
('image-%d-title' % self.image.id): "New title!",
('image-%d-tags' % self.image.id): "",
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Check response
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
# Check JSON
response_json = json.loads(response.content.decode())
self.assertIn('image_id', response_json)
self.assertNotIn('form', response_json)
self.assertIn('success', response_json)
self.assertEqual(response_json['image_id'], self.image.id)
self.assertTrue(response_json['success'])
def test_edit_post_noajax(self):
"""
This tests that a POST request to the edit view without AJAX returns a 400 response
"""
# Send request
response = self.client.post(reverse('wagtailimages_edit_multiple', args=(self.image.id, )), {
('image-%d-title' % self.image.id): "New title!",
('image-%d-tags' % self.image.id): "",
})
# Check response
self.assertEqual(response.status_code, 400)
def test_edit_post_validation_error(self):
"""
This tests that a POST request to the edit page returns a json document with "success=False"
and a form with the validation error indicated
"""
# Send request
response = self.client.post(reverse('wagtailimages_edit_multiple', args=(self.image.id, )), {
('image-%d-title' % self.image.id): "", # Required
('image-%d-tags' % self.image.id): "",
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Check response
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
self.assertTemplateUsed(response, 'wagtailimages/multiple/edit_form.html')
# Check that a form error was raised
self.assertFormError(response, 'form', 'title', "This field is required.")
# Check JSON
response_json = json.loads(response.content.decode())
self.assertIn('image_id', response_json)
self.assertIn('form', response_json)
self.assertIn('success', response_json)
self.assertEqual(response_json['image_id'], self.image.id)
self.assertFalse(response_json['success'])
def test_delete_get(self):
"""
This tests that a GET request to the delete view returns a 405 "METHOD NOT ALLOWED" response
"""
# Send request
response = self.client.get(reverse('wagtailimages_delete_multiple', args=(self.image.id, )))
# Check response
self.assertEqual(response.status_code, 405)
def test_delete_post(self):
"""
This tests that a POST request to the delete view deletes the image
"""
# Send request
response = self.client.post(reverse('wagtailimages_delete_multiple', args=(self.image.id, )), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Check response
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
# Make sure the image is deleted
self.assertFalse(Image.objects.filter(id=self.image.id).exists())
# Check JSON
response_json = json.loads(response.content.decode())
self.assertIn('image_id', response_json)
self.assertIn('success', response_json)
self.assertEqual(response_json['image_id'], self.image.id)
self.assertTrue(response_json['success'])
def test_edit_post_noajax2(self):
"""
This tests that a POST request to the delete view without AJAX returns a 400 response
"""
# Send request
response = self.client.post(reverse('wagtailimages_delete_multiple', args=(self.image.id, )))
# Check response
self.assertEqual(response.status_code, 400)

View file

@ -1,5 +1,5 @@
from django.conf.urls import url
from wagtail.wagtailimages.views import images, chooser
from wagtail.wagtailimages.views import images, chooser, multiple
urlpatterns = [
url(r'^$', images.index, name='wagtailimages_index'),
@ -8,6 +8,10 @@ urlpatterns = [
url(r'^add/$', images.add, name='wagtailimages_add_image'),
url(r'^usage/(\d+)/$', images.usage, name='wagtailimages_image_usage'),
url(r'^multiple/add/$', multiple.add, name='wagtailimages_add_multiple'),
url(r'^multiple/(\d+)/$', multiple.edit, name='wagtailimages_edit_multiple'),
url(r'^multiple/(\d+)/delete/$', multiple.delete, name='wagtailimages_delete_multiple'),
url(r'^chooser/$', chooser.chooser, name='wagtailimages_chooser'),
url(r'^chooser/(\d+)/$', chooser.image_chosen, name='wagtailimages_image_chosen'),
url(r'^chooser/upload/$', chooser.chooser_upload, name='wagtailimages_chooser_upload'),

View file

@ -14,15 +14,22 @@ def validate_image_format(f):
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."))
raise ValidationError(_("Not a valid image. Please use a gif, jpeg or png file with the correct file extension (*.gif, *.jpg or *.png)."))
if not f.closed:
# Open image file
file_position = f.tell()
f.seek(0)
image = Image.open(f)
try:
image = Image.open(f)
except IOError:
# Uploaded file is not even an image file (or corrupted)
raise ValidationError(_("Not a valid image. Please use a gif, jpeg or png file with the correct file extension (*.gif, *.jpg or *.png)."))
f.seek(file_position)
# Check that the internal format matches the extension
# It is possible to upload PSD files if their extension is set to jpg, png or gif. This should catch them out
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()))
raise ValidationError(_("Not a valid %s image. Please use a gif, jpeg or png file with the correct file extension (*.gif, *.jpg or *.png).") % (extension.upper()))

View file

@ -0,0 +1,113 @@
import json
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import permission_required
from django.views.decorators.http import require_POST
from django.core.exceptions import PermissionDenied, ValidationError
from django.views.decorators.vary import vary_on_headers
from django.http import HttpResponse, HttpResponseBadRequest
from django.template import RequestContext
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.forms import get_image_form_for_multi
from wagtail.wagtailimages.utils import validate_image_format
def json_response(document):
return HttpResponse(json.dumps(document), content_type='application/json')
@permission_required('wagtailimages.add_image')
@vary_on_headers('X-Requested-With')
def add(request):
Image = get_image_model()
ImageForm = get_image_form_for_multi()
if request.method == 'POST':
if not request.is_ajax():
return HttpResponseBadRequest("Cannot POST to this view without AJAX")
if not request.FILES:
return HttpResponseBadRequest("Must upload a file")
# Check that the uploaded file is valid
try:
validate_image_format(request.FILES['files[]'])
except ValidationError as e:
return json_response({
'success': False,
'error_message': '\n'.join(e.messages),
})
# Save it
image = Image(uploaded_by_user=request.user, title=request.FILES['files[]'].name, file=request.FILES['files[]'])
image.save()
# Success! Send back an edit form for this image to the user
form = ImageForm(instance=image, prefix='image-%d' % image.id)
return json_response({
'success': True,
'image_id': int(image.id),
'form': render_to_string('wagtailimages/multiple/edit_form.html', {
'image': image,
'form': form,
}, context_instance=RequestContext(request)),
})
return render(request, 'wagtailimages/multiple/add.html', {})
@require_POST
@permission_required('wagtailadmin.access_admin') # more specific permission tests are applied within the view
def edit(request, image_id, callback=None):
Image = get_image_model()
ImageForm = get_image_form_for_multi()
image = get_object_or_404(Image, id=image_id)
if not request.is_ajax():
return HttpResponseBadRequest("Cannot POST to this view without AJAX")
if not image.is_editable_by_user(request.user):
raise PermissionDenied
form = ImageForm(request.POST, request.FILES, instance=image, prefix='image-'+image_id)
if form.is_valid():
form.save()
return json_response({
'success': True,
'image_id': int(image_id),
})
else:
return json_response({
'success': False,
'image_id': int(image_id),
'form': render_to_string('wagtailimages/multiple/edit_form.html', {
'image': image,
'form': form,
}, context_instance=RequestContext(request)),
})
@require_POST
@permission_required('wagtailadmin.access_admin') # more specific permission tests are applied within the view
def delete(request, image_id):
image = get_object_or_404(get_image_model(), id=image_id)
if not request.is_ajax():
return HttpResponseBadRequest("Cannot POST to this view without AJAX")
if not image.is_editable_by_user(request.user):
raise PermissionDenied
image.delete()
return json_response({
'success': True,
'image_id': int(image_id),
})

View file

@ -6,12 +6,14 @@
{% include "wagtailadmin/shared/header.html" with title=add_str icon="pick" %}
<div class="nice-padding">
{% blocktrans %}
<p>Editors picks are a means of recommending specific pages that might not organically come high up in search results. E.g recommending your primary donation page to a user searching with a less common term like "<em>giving</em>".</p>
{% endblocktrans %}
{% blocktrans %}
<p>The "Search term(s)/phrase" field below must contain the full and exact search for which you wish to provide recommended results, <em>including</em> any misspellings/user error. To help, you can choose from search terms that have been popular with users of your site.</p>
{% endblocktrans %}
<div class="help-block help-info">
{% blocktrans %}
<p>Editors picks are a means of recommending specific pages that might not organically come high up in search results. E.g recommending your primary donation page to a user searching with a less common term like "<em>giving</em>".</p>
{% endblocktrans %}
{% blocktrans %}
<p>The "Search term(s)/phrase" field below must contain the full and exact search for which you wish to provide recommended results, <em>including</em> any misspellings/user error. To help, you can choose from search terms that have been popular with users of your site.</p>
{% endblocktrans %}
</div>
<form action="{% url 'wagtailsearch_editorspicks_add' %}" method="POST">
{% csrf_token %}

View file

@ -11,7 +11,7 @@
<tbody>
{% if queries %}
{% for query in queries %}
<tr class="can-choose">
<tr>
<td class="title">
<h2><a class="choose-query" href="#{{ query.id }}" data-id="{{ query.id }}" data-querystring="{{ query.query_string }}">{{ query.query_string }}</a></h2>
</td>