Merge remote-tracking branch 'torchbox/master' into image-smartcropping

Conflicts:
	wagtail/wagtailimages/tests.py
pull/487/head
Karl Hobley 2014-07-24 14:33:50 +01:00
commit 92a433832f
36 zmienionych plików z 3095 dodań i 43 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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
==============================

Wyświetl plik

@ -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,9 @@
<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>Listings used for choosing a list item</h3>
@ -157,16 +165,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 +190,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>

Wyświetl plik

@ -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,
})

Wyświetl plik

@ -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
@ -414,3 +417,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)

Wyświetl plik

@ -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;

Wyświetl plik

@ -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

Wyświetl plik

@ -122,7 +122,7 @@
-webkit-font-smoothing: auto;
font-size:0.80em;
margin:0 0.5em 0.5em;
background:white url( "#{$static-root}bg-dark-diag.svg");
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;
}
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -101,18 +101,38 @@ ul.listing{
&.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;
}
}
}
@ -309,8 +329,8 @@ ul.listing{
padding-left:20px;
}
.inactive h2{
opacity:0.5;
.unpublished h2{
opacity:0.7;
}
.index {

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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";

Wyświetl plik

@ -28,7 +28,7 @@
{% 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>
@ -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 %}

Wyświetl plik

@ -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())

Wyświetl plik

@ -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,

Wyświetl plik

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

Wyświetl plik

@ -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 = (

Wyświetl plik

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

Wyświetl plik

@ -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

Wyświetl plik

@ -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{
}
});
});
});

Wyświetl plik

@ -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);

Wyświetl plik

@ -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;
}
}
});
}));

Wyświetl plik

@ -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();
}
});
}));

Wyświetl plik

@ -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

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -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">

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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>

Wyświetl plik

@ -1,3 +1,5 @@
import json
from mock import MagicMock
from django.utils import six
@ -475,6 +477,212 @@ class TestFormat(TestCase):
self.assertEqual(result, self.format)
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_noajax(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)
class TestCropToPoint(TestCase):
def test_basic(self):
"Test basic cropping in the centre of the image"

Wyświetl plik

@ -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'),
@ -7,6 +7,10 @@ urlpatterns = [
url(r'^(\d+)/delete/$', images.delete, name='wagtailimages_delete_image'),
url(r'^add/$', images.add, name='wagtailimages_add_image'),
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'),

Wyświetl plik

@ -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()))

Wyświetl plik

@ -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),
})

Wyświetl plik

@ -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>