kopia lustrzana https://github.com/wagtail/wagtail
Merge remote-tracking branch 'torchbox/master' into image-smartcropping
Conflicts: wagtail/wagtailimages/tests.pypull/487/head
commit
92a433832f
|
@ -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
|
||||
|
|
|
@ -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
|
||||
==============================
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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!",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}));
|
|
@ -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();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}));
|
Plik diff jest za duży
Load Diff
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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),
|
||||
})
|
|
@ -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>
|
||||
|
|
Ładowanie…
Reference in New Issue