diff --git a/wagtail/wagtailadmin/static/wagtailadmin/css/panels/rich-text.less b/wagtail/wagtailadmin/static/wagtailadmin/css/panels/rich-text.less
index 22b431edf..4c2c5f7be 100644
--- a/wagtail/wagtailadmin/static/wagtailadmin/css/panels/rich-text.less
+++ b/wagtail/wagtailadmin/static/wagtailadmin/css/panels/rich-text.less
@@ -105,7 +105,7 @@
}
/*
- These styles correspond to the image formats defined in verdantimages/formats.py,
+ These styles correspond to the image formats defined in wagtailimages/formats.py,
so that images displayed in the rich text field receive more or less the same
styling that they would receive on the site front-end.
TODO: when we implement a mechanism to configure the image format list on a
diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_css.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_css.html
index c12d8d4b3..3e4ba4e32 100644
--- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_css.html
+++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_css.html
@@ -5,8 +5,8 @@
{% endcomment %}
{% comment %}
- TODO: have a mechanism to specify this include within the verdantimages app -
- ideally wagtailadmin shouldn't have to know anything at all about verdantimages
+ TODO: have a mechanism for sub-apps to specify their own declarations -
+ ideally wagtailadmin shouldn't have to know anything at all about wagtailimages and friends
{% endcomment %}
{% compress css %}
diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html
index 57268720a..cd4752e26 100644
--- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html
+++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html
@@ -13,19 +13,19 @@
-
+
+
{% comment %}
- TODO: have a mechanism to specify this include (and hallo-verdantimage.coffee)
- within the verdantimages app -
- ideally wagtailadmin shouldn't have to know anything at all about verdantimages
+ TODO: have a mechanism to specify image-chooser.js (and hallo-verdantimage.coffee)
+ within the wagtailimages app -
+ ideally wagtailadmin shouldn't have to know anything at all about wagtailimages
TODO: a method of injecting these sorts of things on demand when the modal is spawned.
{% endcomment %}
-
-
+
{% endcompress %}
diff --git a/wagtail/wagtailadmin/templatetags/wagtailadmin_nav.py b/wagtail/wagtailadmin/templatetags/wagtailadmin_nav.py
index 566845678..34f209e64 100644
--- a/wagtail/wagtailadmin/templatetags/wagtailadmin_nav.py
+++ b/wagtail/wagtailadmin/templatetags/wagtailadmin_nav.py
@@ -45,9 +45,9 @@ def main_nav(context):
request = context['request']
user = request.user
- if user.has_perm('verdantimages.add_image'):
+ if user.has_perm('wagtailimages.add_image'):
menu_items.append(
- MenuItem('Images', urlresolvers.reverse('verdantimages_index'), classnames='icon icon-image', order=300)
+ MenuItem('Images', urlresolvers.reverse('wagtailimages_index'), classnames='icon icon-image', order=300)
)
if user.has_perm('wagtaildocs.add_document'):
menu_items.append(
diff --git a/wagtail/wagtailadmin/views/home.py b/wagtail/wagtailadmin/views/home.py
index 2f4091fcd..e4a1d281d 100644
--- a/wagtail/wagtailadmin/views/home.py
+++ b/wagtail/wagtailadmin/views/home.py
@@ -5,7 +5,7 @@ from django.template import RequestContext
from django.template.loader import render_to_string
from wagtail.wagtailcore.models import Page, PageRevision, UserPagePermissionsProxy
-from verdantimages.models import get_image_model
+from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtaildocs.models import Document
from wagtail.wagtailadmin import hooks
diff --git a/wagtail/wagtailcore/rich_text.py b/wagtail/wagtailcore/rich_text.py
index aa997e2c6..94f7b3cb1 100644
--- a/wagtail/wagtailcore/rich_text.py
+++ b/wagtail/wagtailcore/rich_text.py
@@ -5,11 +5,11 @@ import re # parsing HTML with regexes LIKE A BOSS.
from wagtail.wagtailcore.whitelist import Whitelister
from wagtail.wagtailcore.models import Page
-# FIXME: we don't really want to import verdantimages within core.
+# FIXME: we don't really want to import wagtailimages within core.
# For that matter, we probably don't want core to be concerned about translating
# HTML for the benefit of the hallo.js editor...
-from verdantimages.models import get_image_model
-from verdantimages.formats import get_image_format
+from wagtail.wagtailimages.models import get_image_model
+from wagtail.wagtailimages.formats import get_image_format
from wagtail.wagtaildocs.models import Document
diff --git a/wagtail/wagtailimages/__init__.py b/wagtail/wagtailimages/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/wagtail/wagtailimages/admin.py b/wagtail/wagtailimages/admin.py
new file mode 100644
index 000000000..243741374
--- /dev/null
+++ b/wagtail/wagtailimages/admin.py
@@ -0,0 +1,12 @@
+from django.contrib import admin
+from django.conf import settings
+
+from wagtail.wagtailimages.models import Image
+
+if hasattr(settings, 'WAGTAILIMAGES_IMAGE_MODEL') and settings.WAGTAILIMAGES_IMAGE_MODEL != 'wagtailimages.Image':
+ # This installation provides its own custom image class;
+ # to avoid confusion, we won't expose the unused wagtailimages.Image class
+ # in the admin.
+ pass
+else:
+ admin.site.register(Image)
diff --git a/wagtail/wagtailimages/edit_handlers.py b/wagtail/wagtailimages/edit_handlers.py
new file mode 100644
index 000000000..18bebab7c
--- /dev/null
+++ b/wagtail/wagtailimages/edit_handlers.py
@@ -0,0 +1,11 @@
+from wagtail.wagtailadmin.edit_handlers import BaseChooserPanel
+
+class BaseImageChooserPanel(BaseChooserPanel):
+ field_template = "wagtailimages/edit_handlers/image_chooser_panel.html"
+ object_type_name = "image"
+ js_function_name = "createImageChooser"
+
+def ImageChooserPanel(field_name):
+ return type('_ImageChooserPanel', (BaseImageChooserPanel,), {
+ 'field_name': field_name,
+ })
diff --git a/wagtail/wagtailimages/formats.py b/wagtail/wagtailimages/formats.py
new file mode 100644
index 000000000..de0a85bdc
--- /dev/null
+++ b/wagtail/wagtailimages/formats.py
@@ -0,0 +1,87 @@
+from django.conf import settings
+from django.utils.importlib import import_module
+from django.utils.html import escape
+
+class Format(object):
+ def __init__(self, name, label, classnames, filter_spec):
+ self.name = name
+ self.label = label
+ self.classnames = classnames
+ self.filter_spec = filter_spec
+
+ def editor_attributes(self, image, alt_text):
+ """
+ Return string of additional attributes to go on the HTML element
+ when outputting this image within a rich text editor field
+ """
+ return 'data-embedtype="image" data-id="%d" data-format="%s" data-alt="%s" ' % (
+ image.id, self.name, alt_text
+ )
+
+ def image_to_editor_html(self, image, alt_text):
+ return self.image_to_html(
+ image, alt_text, self.editor_attributes(image, alt_text)
+ )
+
+ def image_to_html(self, image, alt_text, extra_attributes=''):
+ rendition = image.get_rendition(self.filter_spec)
+
+ if self.classnames:
+ class_attr = 'class="%s" ' % escape(self.classnames)
+ else:
+ class_attr = ''
+
+ return '' % (
+ extra_attributes, class_attr,
+ escape(rendition.url), rendition.width, rendition.height, alt_text
+ )
+
+
+FORMATS = []
+FORMATS_BY_NAME = {}
+
+def register_image_format(format):
+ if format.name in FORMATS_BY_NAME:
+ raise KeyError("Image format '%s' is already registered" % format.name)
+ FORMATS_BY_NAME[format.name] = format
+ FORMATS.append(format)
+
+def unregister_image_format(format_name):
+ global FORMATS
+ # handle being passed a format object rather than a format name string
+ try:
+ format_name = format_name.name
+ except AttributeError:
+ pass
+
+ try:
+ del FORMATS_BY_NAME[format_name]
+ FORMATS = [fmt for fmt in FORMATS if fmt.name != format_name]
+ except KeyError:
+ raise KeyError("Image format '%s' is not registered" % format_name)
+
+def get_image_formats():
+ search_for_image_formats()
+ return FORMATS
+
+def get_image_format(name):
+ search_for_image_formats()
+ return FORMATS_BY_NAME[name]
+
+_searched_for_image_formats = False
+def search_for_image_formats():
+ global _searched_for_image_formats
+ if not _searched_for_image_formats:
+ for app_module in settings.INSTALLED_APPS:
+ try:
+ import_module('%s.image_formats' % app_module)
+ except ImportError:
+ continue
+
+ _searched_for_image_formats = True
+
+
+# Define default image formats
+register_image_format(Format('fullwidth', 'Full width', 'full-width', 'width-800'))
+register_image_format(Format('left', 'Left-aligned', 'left', 'width-500'))
+register_image_format(Format('right', 'Right-aligned', 'right', 'width-500'))
diff --git a/wagtail/wagtailimages/forms.py b/wagtail/wagtailimages/forms.py
new file mode 100644
index 000000000..fd3345c03
--- /dev/null
+++ b/wagtail/wagtailimages/forms.py
@@ -0,0 +1,25 @@
+from django import forms
+from django.forms.models import modelform_factory
+
+from wagtail.wagtailimages.models import get_image_model
+from wagtail.wagtailimages.formats import get_image_formats
+
+
+def get_image_form():
+ return modelform_factory(get_image_model(),
+ # set the 'file' widget to a FileInput rather than the default ClearableFileInput
+ # so that when editing, we don't get the 'currently: ...' banner which is
+ # a bit pointless here
+ widgets = {'file': forms.FileInput()})
+
+
+class ImageInsertionForm(forms.Form):
+ """
+ Form for selecting parameters of the image (e.g. format) prior to insertion
+ into a rich text area
+ """
+ format = forms.ChoiceField(
+ choices=[(format.name, format.label) for format in get_image_formats()],
+ widget=forms.RadioSelect
+ )
+ alt_text = forms.CharField()
diff --git a/wagtail/wagtailimages/image_ops.py b/wagtail/wagtailimages/image_ops.py
new file mode 100644
index 000000000..ecc7a6526
--- /dev/null
+++ b/wagtail/wagtailimages/image_ops.py
@@ -0,0 +1,123 @@
+from PIL import Image
+
+def resize(image, size):
+ """
+ resize image to the requested size, using highest quality settings
+ (antialiasing enabled, converting to true colour if required)
+ """
+ if image.mode in ['1', 'P']:
+ image = image.convert('RGB')
+
+ return image.resize(size, Image.ANTIALIAS)
+
+
+def crop_to_centre(image, size):
+ (original_width, original_height) = image.size
+ (target_width, target_height) = size
+
+ # final dimensions should not exceed original dimensions
+ final_width = min(original_width, target_width)
+ final_height = min(original_height, target_height)
+
+ if final_width == original_width and final_height == original_height:
+ return image
+
+ left = (original_width - final_width) / 2
+ top = (original_height - final_height) / 2
+ return image.crop(
+ (left, top, left + final_width, top + final_height)
+ )
+
+
+def resize_to_max(image, size):
+ """
+ Resize image down to fit within the given dimensions, preserving aspect ratio.
+ Will leave image unchanged if it's already within those dimensions.
+ """
+ (original_width, original_height) = image.size
+ (target_width, target_height) = size
+
+ if original_width <= target_width and original_height <= target_height:
+ return image
+
+ # scale factor if we were to downsize the image to fit the target width
+ horz_scale = float(target_width) / original_width
+ # scale factor if we were to downsize the image to fit the target height
+ vert_scale = float(target_height) / original_height
+
+ # choose whichever of these gives a smaller image
+ if horz_scale < vert_scale:
+ final_size = (target_width, int(original_height * horz_scale))
+ else:
+ final_size = (int(original_width * vert_scale), target_height)
+
+ return resize(image, final_size)
+
+
+def resize_to_min(image, size):
+ """
+ Resize image down to cover the given dimensions, preserving aspect ratio.
+ Will leave image unchanged if width or height is already within those limits.
+ """
+ (original_width, original_height) = image.size
+ (target_width, target_height) = size
+
+ if original_width <= target_width or original_height <= target_height:
+ return image
+
+ # scale factor if we were to downsize the image to fit the target width
+ horz_scale = float(target_width) / original_width
+ # scale factor if we were to downsize the image to fit the target height
+ vert_scale = float(target_height) / original_height
+
+ # choose whichever of these gives a larger image
+ if horz_scale > vert_scale:
+ final_size = (target_width, int(original_height * horz_scale))
+ else:
+ final_size = (int(original_width * vert_scale), target_height)
+
+ return resize(image, final_size)
+
+
+def resize_to_width(image, target_width):
+ """
+ Resize image down to the given width, preserving aspect ratio.
+ Will leave image unchanged if it's already within that width.
+ """
+ (original_width, original_height) = image.size
+
+ if original_width <= target_width:
+ return image
+
+ scale = float(target_width) / original_width
+
+ final_size = (target_width, int(original_height * scale))
+
+ return resize(image, final_size)
+
+
+def resize_to_height(image, target_height):
+ """
+ Resize image down to the given height, preserving aspect ratio.
+ Will leave image unchanged if it's already within that height.
+ """
+ (original_width, original_height) = image.size
+
+ if original_height <= target_height:
+ return image
+
+ scale = float(target_height) / original_height
+
+ final_size = (int(original_width * scale), target_height)
+
+ return resize(image, final_size)
+
+
+def resize_to_fill(image, size):
+ """
+ Resize down and crop image to fill the given dimensions. Most suitable for thumbnails.
+ (The final image will match the requested size, unless one or the other dimension is
+ already smaller than the target size)
+ """
+ resized_image = resize_to_min(image, size)
+ return crop_to_centre(resized_image, size)
diff --git a/wagtail/wagtailimages/migrations/0001_initial.py b/wagtail/wagtailimages/migrations/0001_initial.py
new file mode 100644
index 000000000..49d0f4a2c
--- /dev/null
+++ b/wagtail/wagtailimages/migrations/0001_initial.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ depends_on = (
+ ("wagtailcore", "0002_initial_data"),
+ )
+
+ def forwards(self, orm):
+ # Adding model 'Image'
+ db.create_table(u'wagtailimages_image', (
+ (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('file', self.gf('django.db.models.fields.files.ImageField')(max_length=100)),
+ ('width', self.gf('django.db.models.fields.IntegerField')()),
+ ('height', self.gf('django.db.models.fields.IntegerField')()),
+ ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ('uploaded_by_user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
+ ))
+ db.send_create_signal(u'wagtailimages', ['Image'])
+
+ # Adding model 'Filter'
+ db.create_table(u'wagtailimages_filter', (
+ (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('spec', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+ ))
+ db.send_create_signal(u'wagtailimages', ['Filter'])
+
+ # Adding model 'Rendition'
+ db.create_table(u'wagtailimages_rendition', (
+ (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('filter', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', to=orm['wagtailimages.Filter'])),
+ ('file', self.gf('django.db.models.fields.files.ImageField')(max_length=100)),
+ ('width', self.gf('django.db.models.fields.IntegerField')()),
+ ('height', self.gf('django.db.models.fields.IntegerField')()),
+ ('image', self.gf('django.db.models.fields.related.ForeignKey')(related_name='renditions', to=orm['wagtailimages.Image'])),
+ ))
+ db.send_create_signal(u'wagtailimages', ['Rendition'])
+
+ # Adding unique constraint on 'Rendition', fields ['image', 'filter']
+ db.create_unique(u'wagtailimages_rendition', ['image_id', 'filter_id'])
+
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'Rendition', fields ['image', 'filter']
+ db.delete_unique(u'wagtailimages_rendition', ['image_id', 'filter_id'])
+
+ # Deleting model 'Image'
+ db.delete_table(u'wagtailimages_image')
+
+ # Deleting model 'Filter'
+ db.delete_table(u'wagtailimages_filter')
+
+ # Deleting model 'Rendition'
+ db.delete_table(u'wagtailimages_rendition')
+
+
+ models = {
+ u'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ u'auth.permission': {
+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ u'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ u'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ u'wagtailimages.filter': {
+ 'Meta': {'object_name': 'Filter'},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'spec': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+ },
+ u'wagtailimages.image': {
+ 'Meta': {'object_name': 'Image'},
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
+ 'height': ('django.db.models.fields.IntegerField', [], {}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'uploaded_by_user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'width': ('django.db.models.fields.IntegerField', [], {})
+ },
+ u'wagtailimages.rendition': {
+ 'Meta': {'unique_together': "(('image', 'filter'),)", 'object_name': 'Rendition'},
+ 'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
+ 'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': u"orm['wagtailimages.Filter']"}),
+ 'height': ('django.db.models.fields.IntegerField', [], {}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'image': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'renditions'", 'to': u"orm['wagtailimages.Image']"}),
+ 'width': ('django.db.models.fields.IntegerField', [], {})
+ }
+ }
+
+ complete_apps = ['wagtailimages']
\ No newline at end of file
diff --git a/wagtail/wagtailimages/migrations/0002_initial_data.py b/wagtail/wagtailimages/migrations/0002_initial_data.py
new file mode 100644
index 000000000..659b8c9c2
--- /dev/null
+++ b/wagtail/wagtailimages/migrations/0002_initial_data.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ image_content_type, created = orm['contenttypes.ContentType'].objects.get_or_create(
+ model='image', app_label='wagtailimages', defaults={'name': 'image'})
+ add_permission, created = orm['auth.permission'].objects.get_or_create(
+ content_type=image_content_type, codename='add_image', defaults=dict(name=u'Can add image'))
+ change_permission, created = orm['auth.permission'].objects.get_or_create(
+ content_type=image_content_type, codename='change_image', defaults=dict(name=u'Can change image'))
+ delete_permission, created = orm['auth.permission'].objects.get_or_create(
+ content_type=image_content_type, codename='delete_image', defaults=dict(name=u'Can delete image'))
+
+ editors_group = orm['auth.group'].objects.get(name='Editors')
+ editors_group.permissions.add(add_permission, change_permission, delete_permission)
+
+ moderators_group = orm['auth.group'].objects.get(name='Moderators')
+ moderators_group.permissions.add(add_permission, change_permission, delete_permission)
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+
+ models = {
+ u'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ u'auth.permission': {
+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ u'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ u'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ u'wagtailimages.filter': {
+ 'Meta': {'object_name': 'Filter'},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'spec': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+ },
+ u'wagtailimages.image': {
+ 'Meta': {'object_name': 'Image'},
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
+ 'height': ('django.db.models.fields.IntegerField', [], {}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'uploaded_by_user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'width': ('django.db.models.fields.IntegerField', [], {})
+ },
+ u'wagtailimages.rendition': {
+ 'Meta': {'unique_together': "(('image', 'filter'),)", 'object_name': 'Rendition'},
+ 'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
+ 'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': u"orm['wagtailimages.Filter']"}),
+ 'height': ('django.db.models.fields.IntegerField', [], {}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'image': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'renditions'", 'to': u"orm['wagtailimages.Image']"}),
+ 'width': ('django.db.models.fields.IntegerField', [], {})
+ }
+ }
+
+ complete_apps = ['wagtailimages']
+ symmetrical = True
diff --git a/wagtail/wagtailimages/migrations/__init__.py b/wagtail/wagtailimages/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/wagtail/wagtailimages/models.py b/wagtail/wagtailimages/models.py
new file mode 100644
index 000000000..2e41f0bed
--- /dev/null
+++ b/wagtail/wagtailimages/models.py
@@ -0,0 +1,241 @@
+from django.core.files import File
+from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, ValidationError
+from django.db import models
+from django.db.models.signals import pre_delete
+from django.dispatch.dispatcher import receiver
+from django.utils.safestring import mark_safe
+from django.utils.html import escape
+
+import StringIO
+import PIL.Image
+import os.path
+
+from taggit.managers import TaggableManager
+
+from wagtail.wagtailadmin.taggable import TagSearchable
+from wagtail.wagtailimages import image_ops
+
+
+class AbstractImage(models.Model, TagSearchable):
+ title = models.CharField(max_length=255)
+
+ def get_upload_to(self, filename):
+ folder_name = 'original_images'
+ filename = self.file.field.storage.get_valid_name(filename)
+
+ # replace non-ascii characters in filename with _ , to sidestep issues with filesystem encoding
+ filename = "".join((i if ord(i)<128 else '_') for i in filename)
+
+ while len(os.path.join(folder_name, filename)) >= 95:
+ prefix, dot, extension = filename.rpartition('.')
+ filename = prefix[:-1] + dot + extension
+ return os.path.join(folder_name, filename)
+
+ def file_extension_validator(ffile):
+ extension = ffile.name.split(".")[-1].lower()
+ if extension not in ["gif", "jpg", "jpeg", "png"]:
+ raise ValidationError("Not a valid image format. Please use a gif, jpeg or png file instead.")
+
+ file = models.ImageField(upload_to=get_upload_to, width_field='width', height_field='height', validators=[file_extension_validator])
+ width = models.IntegerField(editable=False)
+ height = models.IntegerField(editable=False)
+ created_at = models.DateTimeField(auto_now_add=True)
+ uploaded_by_user = models.ForeignKey('auth.User', null=True, blank=True, editable=False)
+
+ tags = TaggableManager(help_text=None, blank=True)
+
+ indexed_fields = {
+ 'uploaded_by_user_id': {
+ 'type': 'integer',
+ 'store': 'yes',
+ 'indexed': 'no',
+ 'boost': 0,
+ },
+ }
+
+ def __unicode__(self):
+ return self.title
+
+ def get_rendition(self, filter):
+ if not hasattr(filter, 'process_image'):
+ # assume we've been passed a filter spec string, rather than a Filter object
+ # TODO: keep an in-memory cache of filters, to avoid a db lookup
+ filter, created = Filter.objects.get_or_create(spec=filter)
+
+ try:
+ rendition = self.renditions.get(filter=filter)
+ except ObjectDoesNotExist:
+ file_field = self.file
+ generated_image_file = filter.process_image(file_field.file)
+
+ rendition, created = self.renditions.get_or_create(
+ filter=filter, defaults={'file': generated_image_file})
+
+ return rendition
+
+ def is_portrait(self):
+ return (self.width < self.height)
+ def is_landscape(self):
+ return (self.height < self.width)
+
+ @property
+ def filename(self):
+ return os.path.basename(self.file.name)
+
+ @property
+ def default_alt_text(self):
+ # by default the alt text field (used in rich text insertion) is populated
+ # from the title. Subclasses might provide a separate alt field, and
+ # override this
+ return self.title
+
+ def is_editable_by_user(self, user):
+ if user.has_perm('wagtailimages.change_image'):
+ # user has global permission to change images
+ return True
+ elif user.has_perm('wagtailimages.add_image') and self.uploaded_by_user == user:
+ # user has image add permission, which also implicitly provides permission to edit their own images
+ return True
+ else:
+ return False
+
+ class Meta:
+ abstract=True
+
+class Image(AbstractImage):
+ pass
+
+ class Meta:
+ permissions = (
+ ('add_image', "Can add image"),
+ ('change_image', "Can change image"),
+ ('delete_image', "Can delete image"),
+ )
+
+
+# Receive the pre_delete signal and delete the file associated with the model instance.
+@receiver(pre_delete, sender=Image)
+def image_delete(sender, instance, **kwargs):
+ # Pass false so FileField doesn't save the model.
+ instance.file.delete(False)
+
+
+def get_image_model():
+ from django.conf import settings
+ from django.db.models import get_model
+
+ try:
+ app_label, model_name = settings.WAGTAILIMAGES_IMAGE_MODEL.split('.')
+ except AttributeError:
+ return Image
+ except ValueError:
+ raise ImproperlyConfigured("WAGTAILIMAGES_IMAGE_MODEL must be of the form 'app_label.model_name'")
+
+ image_model = get_model(app_label, model_name)
+ if image_model is None:
+ raise ImproperlyConfigured("WAGTAILIMAGES_IMAGE_MODEL refers to model '%s' that has not been installed" % settings.WAGTAILIMAGES_IMAGE_MODEL)
+ return image_model
+
+
+class Filter(models.Model):
+ """
+ Represents an operation that can be applied to an Image to produce a rendition
+ appropriate for final display on the website. Usually this would be a resize operation,
+ but could potentially involve colour processing, etc.
+ """
+ spec = models.CharField(max_length=255, db_index=True)
+
+ OPERATION_NAMES = {
+ 'max': image_ops.resize_to_max,
+ 'min': image_ops.resize_to_min,
+ 'width': image_ops.resize_to_width,
+ 'height': image_ops.resize_to_height,
+ 'fill': image_ops.resize_to_fill,
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(Filter, self).__init__(*args, **kwargs)
+ self.method = None # will be populated when needed, by parsing the spec string
+
+ def _parse_spec_string(self):
+ # parse the spec string, which is formatted as (method)-(arg),
+ # and save the results to self.method and self.method_arg
+ try:
+ (method_name, method_arg_string) = self.spec.split('-')
+ self.method = Filter.OPERATION_NAMES[method_name]
+
+ if method_name in ('max', 'min', 'fill'):
+ # method_arg_string is in the form 640x480
+ (width, height) = [int(i) for i in method_arg_string.split('x')]
+ self.method_arg = (width, height)
+ else:
+ # method_arg_string is a single number
+ self.method_arg = int(method_arg_string)
+
+ except (ValueError, KeyError):
+ raise ValueError("Invalid image filter spec: %r" % self.spec)
+
+ def process_image(self, input_file):
+ """
+ Given an input image file as a django.core.files.File object,
+ generate an output image with this filter applied, returning it
+ as another django.core.files.File object
+ """
+ if not self.method:
+ self._parse_spec_string()
+
+ input_file.open()
+ image = PIL.Image.open(input_file)
+ file_format = image.format
+
+ # perform the resize operation
+ image = self.method(image, self.method_arg)
+
+ output = StringIO.StringIO()
+ image.save(output, file_format)
+
+ # generate new filename derived from old one, inserting the filter spec string before the extension
+ input_filename_parts = os.path.basename(input_file.name).split('.')
+ filename_without_extension = '.'.join(input_filename_parts[:-1])
+ filename_without_extension = filename_without_extension[:60] # trim filename base so that we're well under 100 chars
+ output_filename_parts = [filename_without_extension, self.spec] + input_filename_parts[-1:]
+ output_filename = '.'.join(output_filename_parts)
+
+ output_file = File(output, name=output_filename)
+ input_file.close()
+
+ return output_file
+
+
+class AbstractRendition(models.Model):
+ filter = models.ForeignKey('Filter', related_name='+')
+ file = models.ImageField(upload_to='images', width_field='width', height_field='height')
+ width = models.IntegerField(editable=False)
+ height = models.IntegerField(editable=False)
+
+ @property
+ def url(self):
+ return self.file.url
+
+ def img_tag(self):
+ return mark_safe(
+ '
' % (escape(self.url), self.width, self.height, escape(self.image.title))
+ )
+
+ class Meta:
+ abstract=True
+
+
+class Rendition(AbstractRendition):
+ image = models.ForeignKey('Image', related_name='renditions')
+
+ class Meta:
+ unique_together = (
+ ('image', 'filter'),
+ )
+
+# Receive the pre_delete signal and delete the file associated with the model instance.
+@receiver(pre_delete, sender=Rendition)
+def rendition_delete(sender, instance, **kwargs):
+ # Pass false so FileField doesn't save the model.
+ instance.file.delete(False)
diff --git a/wagtail/wagtailimages/static/wagtailimages/js/hallo-plugins/hallo-verdantimage.coffee b/wagtail/wagtailimages/static/wagtailimages/js/hallo-plugins/hallo-verdantimage.coffee
new file mode 100644
index 000000000..57f097867
--- /dev/null
+++ b/wagtail/wagtailimages/static/wagtailimages/js/hallo-plugins/hallo-verdantimage.coffee
@@ -0,0 +1,39 @@
+# plugin for hallo.js to allow inserting images from the Verdant image library
+
+(($) ->
+ $.widget "IKS.halloverdantimage",
+ options:
+ uuid: ''
+ editable: null
+
+ populateToolbar: (toolbar) ->
+ widget = this
+
+ # Create an element for holding the button
+ button = $('')
+ button.hallobutton
+ uuid: @options.uuid
+ editable: @options.editable
+ label: 'Images'
+ icon: 'icon-picture'
+ command: null
+
+ # Append the button to toolbar
+ toolbar.append button
+
+ button.on "click", (event) ->
+ lastSelection = widget.options.editable.getSelection()
+ insertionPoint = $(lastSelection.endContainer).parentsUntil('.richtext').last()
+ ModalWorkflow
+ url: '/admin/images/chooser/?select_format=true' # TODO: don't hard-code this, as it may be changed in urls.py
+ responses:
+ imageChosen: (imageData) ->
+ elem = $(imageData.html).get(0)
+
+ lastSelection.insertNode(elem)
+
+ if elem.getAttribute('contenteditable') == 'false'
+ insertRichTextDeleteControl(elem)
+ widget.options.editable.element.trigger('change')
+
+)(jQuery)
diff --git a/wagtail/wagtailimages/static/wagtailimages/js/image-chooser.js b/wagtail/wagtailimages/static/wagtailimages/js/image-chooser.js
new file mode 100644
index 000000000..cd340b1ca
--- /dev/null
+++ b/wagtail/wagtailimages/static/wagtailimages/js/image-chooser.js
@@ -0,0 +1,28 @@
+function createImageChooser(id) {
+ var chooserElement = $('#' + id + '-chooser');
+ var previewImage = chooserElement.find('.preview-image img');
+ var input = $('#' + id);
+
+ $('.action-choose', chooserElement).click(function() {
+ ModalWorkflow({
+ 'url': '/admin/images/chooser/', /* TODO: don't hard-code this, as it may be changed in urls.py */
+ 'responses': {
+ 'imageChosen': function(imageData) {
+ input.val(imageData.id);
+ previewImage.attr({
+ 'src': imageData.preview.url,
+ 'width': imageData.preview.width,
+ 'height': imageData.preview.height,
+ 'alt': imageData.title
+ });
+ chooserElement.removeClass('blank');
+ }
+ }
+ });
+ });
+
+ $('.action-clear', chooserElement).click(function() {
+ input.val('');
+ chooserElement.addClass('blank');
+ });
+}
diff --git a/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html b/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html
new file mode 100644
index 000000000..3736c818d
--- /dev/null
+++ b/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html
@@ -0,0 +1,49 @@
+{% load image_tags ellipsistrim%}
+
+
Choose an image
+
Are you sure you want to delete this image?
+ +Sorry, no images match "{{ search_query }}"
+ {% else %} +You've not uploaded any images. Why not add one now?
+ {% endif %} +{% endif %} diff --git a/wagtail/wagtailimages/templatetags/__init__.py b/wagtail/wagtailimages/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wagtail/wagtailimages/templatetags/image_tags.py b/wagtail/wagtailimages/templatetags/image_tags.py new file mode 100644 index 000000000..0fee50ec9 --- /dev/null +++ b/wagtail/wagtailimages/templatetags/image_tags.py @@ -0,0 +1,51 @@ +from django import template + +from wagtail.wagtailimages.models import Filter + +register = template.Library() + +@register.tag(name="image") +def image(parser, token): + args = token.split_contents() + + if len(args) == 3: + # token is of the form {% image self.photo max-320x200 %} + tag_name, image_var, filter_spec = args + return ImageNode(image_var, filter_spec) + + elif len(args) == 5: + # token is of the form {% image self.photo max-320x200 as img %} + tag_name, image_var, filter_spec, as_token, out_var = args + + if as_token != 'as': + raise template.TemplateSyntaxError("'image' tag should be of the form {%% image self.photo max-320x200 %%} or {%% image self.photo max-320x200 as img %%}") + + return ImageNode(image_var, filter_spec, out_var) + + else: + raise template.TemplateSyntaxError("'image' tag should be of the form {%% image self.photo max-320x200 %%} or {%% image self.photo max-320x200 as img %%}") + +class ImageNode(template.Node): + def __init__(self, image_var_name, filter_spec, output_var_name=None): + self.image_var = template.Variable(image_var_name) + self.filter, created = Filter.objects.get_or_create(spec=filter_spec) + self.output_var_name = output_var_name + + def render(self, context): + try: + image = self.image_var.resolve(context) + except template.VariableDoesNotExist: + return '' + + if not image: + return '' + + rendition = image.get_rendition(self.filter) + + if self.output_var_name: + # return the rendition object in the given variable + context[self.output_var_name] = rendition + return '' + else: + # render the rendition's image tag now + return rendition.img_tag() diff --git a/wagtail/wagtailimages/tests.py b/wagtail/wagtailimages/tests.py new file mode 100644 index 000000000..501deb776 --- /dev/null +++ b/wagtail/wagtailimages/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/wagtail/wagtailimages/urls.py b/wagtail/wagtailimages/urls.py new file mode 100644 index 000000000..206974f97 --- /dev/null +++ b/wagtail/wagtailimages/urls.py @@ -0,0 +1,15 @@ +from django.conf.urls import patterns, url + + +urlpatterns = patterns('wagtail.wagtailimages.views', + url(r'^$', 'images.index', name='wagtailimages_index'), + url(r'^(\d+)/$', 'images.edit', name='wagtailimages_edit_image'), + url(r'^(\d+)/delete/$', 'images.delete', name='wagtailimages_delete_image'), + url(r'^add/$', 'images.add', name='wagtailimages_add_image'), + url(r'^search/$', 'images.search', name='wagtailimages_search_image'), + + 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'), + url(r'^chooser/(\d+)/select_format/$', 'chooser.chooser_select_format', name='wagtailimages_chooser_select_format'), +) diff --git a/wagtail/wagtailimages/views/__init__.py b/wagtail/wagtailimages/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wagtail/wagtailimages/views/chooser.py b/wagtail/wagtailimages/views/chooser.py new file mode 100644 index 000000000..71f26c45d --- /dev/null +++ b/wagtail/wagtailimages/views/chooser.py @@ -0,0 +1,177 @@ +from django.shortcuts import get_object_or_404, render +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.contrib.auth.decorators import login_required, permission_required + +import json + +from wagtail.wagtailadmin.modal_workflow import render_modal_workflow +from wagtail.wagtailimages.models import get_image_model +from wagtail.wagtailimages.forms import get_image_form, ImageInsertionForm +from wagtail.wagtailadmin.forms import SearchForm +from wagtail.wagtailimages.formats import get_image_format + + +def get_image_json(image): + """ + helper function: given an image, return the json to pass back to the + image chooser panel + """ + preview_image = image.get_rendition('max-130x100') + + return json.dumps({ + 'id': image.id, + 'title': image.title, + 'preview': { + 'url': preview_image.url, + 'width': preview_image.width, + 'height': preview_image.height, + } + }) + +@login_required +def chooser(request): + Image = get_image_model() + + if request.user.has_perm('wagtailimages.add_image'): + ImageForm = get_image_form() + uploadform = ImageForm() + else: + uploadform = None + + q = None + if 'q' in request.GET or 'p' in request.GET: + searchform = SearchForm(request.GET) + if searchform.is_valid(): + q = searchform.cleaned_data['q'] + + # page number + p = request.GET.get("p", 1) + + images = Image.search(q, results_per_page=10, page=p) + + is_searching = True + + else: + images = Image.objects.order_by('-created_at') + p = request.GET.get("p", 1) + paginator = Paginator(images, 10) + + try: + images = paginator.page(p) + except PageNotAnInteger: + images = paginator.page(1) + except EmptyPage: + images = paginator.page(paginator.num_pages) + + is_searching = False + + return render(request, "wagtailimages/chooser/results.html", { + 'images': images, + 'is_searching': is_searching, + 'will_select_format': request.GET.get('select_format') + }) + else: + searchform = SearchForm() + + images = Image.objects.order_by('-created_at') + p = request.GET.get("p", 1) + paginator = Paginator(images, 10) + + try: + images = paginator.page(p) + except PageNotAnInteger: + images = paginator.page(1) + except EmptyPage: + images = paginator.page(paginator.num_pages) + + + return render_modal_workflow(request, 'wagtailimages/chooser/chooser.html', 'wagtailimages/chooser/chooser.js',{ + 'images': images, + 'uploadform': uploadform, + 'searchform': searchform, + 'is_searching': False, + 'will_select_format': request.GET.get('select_format'), + 'popular_tags': Image.popular_tags(), + }) + + +@login_required +def image_chosen(request, image_id): + image = get_object_or_404(get_image_model(), id=image_id) + + return render_modal_workflow( + request, None, 'wagtailimages/chooser/image_chosen.js', + {'image_json': get_image_json(image)} + ) + + +@permission_required('wagtailimages.add_image') +def chooser_upload(request): + Image = get_image_model() + ImageForm = get_image_form() + + if request.POST: + image = Image(uploaded_by_user=request.user) + form = ImageForm(request.POST, request.FILES, instance=image) + + if form.is_valid(): + form.save() + if request.GET.get('select_format'): + form = ImageInsertionForm(initial={'alt_text': image.default_alt_text}) + return render_modal_workflow( + request, 'wagtailimages/chooser/select_format.html', 'wagtailimages/chooser/select_format.js', + {'image': image, 'form': form} + ) + else: + # not specifying a format; return the image details now + return render_modal_workflow( + request, None, 'wagtailimages/chooser/image_chosen.js', + {'image_json': get_image_json(image)} + ) + else: + form = ImageForm() + + images = Image.objects.order_by('title') + + return render_modal_workflow( + request, 'wagtailimages/chooser/chooser.html', 'wagtailimages/chooser/chooser.js', + {'images': images, 'uploadform': form} + ) + + +@login_required +def chooser_select_format(request, image_id): + image = get_object_or_404(get_image_model(), id=image_id) + + if request.POST: + form = ImageInsertionForm(request.POST, initial={'alt_text': image.default_alt_text}) + if form.is_valid(): + + format = get_image_format(form.cleaned_data['format']) + preview_image = image.get_rendition(format.filter_spec) + + image_json = json.dumps({ + 'id': image.id, + 'title': image.title, + 'format': format.name, + 'alt': form.cleaned_data['alt_text'], + 'class': format.classnames, + 'preview': { + 'url': preview_image.url, + 'width': preview_image.width, + 'height': preview_image.height, + }, + 'html': format.image_to_editor_html(image, form.cleaned_data['alt_text']), + }) + + return render_modal_workflow( + request, None, 'wagtailimages/chooser/image_chosen.js', + {'image_json': image_json} + ) + else: + form = ImageInsertionForm(initial={'alt_text': image.default_alt_text}) + + return render_modal_workflow( + request, 'wagtailimages/chooser/select_format.html', 'wagtailimages/chooser/select_format.js', + {'image': image, 'form': form} + ) diff --git a/wagtail/wagtailimages/views/images.py b/wagtail/wagtailimages/views/images.py new file mode 100644 index 000000000..f8d9eeb13 --- /dev/null +++ b/wagtail/wagtailimages/views/images.py @@ -0,0 +1,173 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib import messages +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.contrib.auth.decorators import permission_required, login_required +from django.core.exceptions import PermissionDenied + +from wagtail.wagtailimages.models import get_image_model +from wagtail.wagtailimages.forms import get_image_form +from wagtail.wagtailadmin.forms import SearchForm + +@permission_required('wagtailimages.add_image') +def index(request): + Image = get_image_model() + + q = None + p = request.GET.get("p", 1) + is_searching = False + + if 'q' in request.GET: + form = SearchForm(request.GET) + if form.is_valid(): + q = form.cleaned_data['q'] + + is_searching = True + if not request.user.has_perm('wagtailimages.change_image'): + # restrict to the user's own images + images = Image.search(q, results_per_page=20, page=p, filters={'uploaded_by_user_id': request.user.id}) + else: + images = Image.search(q, results_per_page=20, page=p) + else: + images = Image.objects.order_by('-created_at') + if not request.user.has_perm('wagtailimages.change_image'): + # restrict to the user's own images + images = images.filter(uploaded_by_user=request.user) + else: + images = Image.objects.order_by('-created_at') + if not request.user.has_perm('wagtailimages.change_image'): + # restrict to the user's own images + images = images.filter(uploaded_by_user=request.user) + form = SearchForm() + + if not is_searching: + paginator = Paginator(images, 20) + + try: + images = paginator.page(p) + except PageNotAnInteger: + images = paginator.page(1) + except EmptyPage: + images = paginator.page(paginator.num_pages) + + if request.is_ajax(): + return render(request, "wagtailimages/images/results.html", { + 'images': images, + 'is_searching': is_searching, + 'search_query': q, + }) + else: + return render(request, "wagtailimages/images/index.html", { + 'form': form, + 'images': images, + 'is_searching': is_searching, + 'popular_tags': Image.popular_tags(), + 'search_query': q, + }) + + +@login_required # more specific permission tests are applied within the view +def edit(request, image_id): + Image = get_image_model() + ImageForm = get_image_form() + + image = get_object_or_404(Image, id=image_id) + + if not image.is_editable_by_user(request.user): + raise PermissionDenied + + if request.POST: + original_file = image.file + form = ImageForm(request.POST, request.FILES, instance=image) + if form.is_valid(): + if 'file' in form.changed_data: + # if providing a new image file, delete the old one and all renditions. + # NB Doing this via original_file.delete() clears the file field, + # which definitely isn't what we want... + original_file.storage.delete(original_file.name) + image.renditions.all().delete() + form.save() + messages.success(request, "Image '%s' updated." % image.title) + return redirect('wagtailimages_index') + else: + messages.error(request, "The image could not be saved due to errors.") + else: + form = ImageForm(instance=image) + + return render(request, "wagtailimages/images/edit.html", { + 'image': image, + 'form': form, + }) + + +@login_required # 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 image.is_editable_by_user(request.user): + raise PermissionDenied + + if request.POST: + image.delete() + messages.success(request, "Image '%s' deleted." % image.title) + return redirect('wagtailimages_index') + + return render(request, "wagtailimages/images/confirm_delete.html", { + 'image': image, + }) + + +@permission_required('wagtailimages.add_image') +def add(request): + ImageForm = get_image_form() + ImageModel = get_image_model() + + if request.POST: + image = ImageModel(uploaded_by_user=request.user) + form = ImageForm(request.POST, request.FILES, instance=image) + if form.is_valid(): + form.save() + messages.success(request, "Image '%s' added." % image.title) + return redirect('wagtailimages_index') + else: + messages.error(request, "The image could not be created due to errors.") + else: + form = ImageForm() + + return render(request, "wagtailimages/images/add.html", { + 'form': form, + }) + + +@permission_required('wagtailimages.add_image') +def search(request): + Image = get_image_model() + images = [] + q = None + is_searching = False + + if 'q' in request.GET: + form = SearchForm(request.GET) + if form.is_valid(): + q = form.cleaned_data['q'] + + # page number + p = request.GET.get("p", 1) + is_searching = True + images = Image.search(q, results_per_page=20, page=p) + else: + form = SearchForm() + + if request.is_ajax(): + return render(request, "wagtailimages/images/results.html", { + 'images': images, + 'is_searching': is_searching, + 'search_query': q, + }) + else: + return render(request, "wagtailimages/images/index.html", { + 'form': form, + 'images': images, + 'is_searching': is_searching, + 'popular_tags': Image.popular_tags(), + 'search_query': q, + })