Skip to content

Commit

Permalink
Add image preview to FileField
Browse files Browse the repository at this point in the history
Use file:// backend as temporary file storage.
Also you can setup cache backend for you project in settings.py, for example:
DJANGO_RESUBMIT_BACKEND = "file://tmp/?timeout=600"
For more information about cache see Django documentation.
  • Loading branch information
un1t committed May 31, 2011
1 parent 7744951 commit bb7f62b
Show file tree
Hide file tree
Showing 22 changed files with 594 additions and 54 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -2,3 +2,4 @@ python
*.egg-info
*.pyc
example/db.sqlite
example/media
3 changes: 3 additions & 0 deletions AUTHORS
@@ -0,0 +1,3 @@
Andrey Podoplelov
Evgeny V. Generalov
Ilya Shalyapin
6 changes: 3 additions & 3 deletions LICENSE
@@ -1,4 +1,4 @@
Copyright 2011 Andrey Podoplelov and Evgeny V. Generalov. All rights reserved.
Copyright 2011. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Expand All @@ -10,7 +10,7 @@ modification, are permitted provided that the following conditions are met:
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY ANDREY PODOPLELOV AND EVGENY V. GENERALOV ``AS
THIS SOFTWARE IS PROVIDED BY DJANGO RESUBMIT TEAM ``AS
IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL ANDREY PODOPLELOV AND EVGENY V. GENERALOV OR
Expand All @@ -24,4 +24,4 @@ OF SUCH DAMAGE.

The views and conclusions contained in the software and documentation are those
of the authors and should not be interpreted as representing official policies,
either expressed or implied, of Andrey Podoplelov and Evgeny V. Generalov.
either expressed or implied, of Django Resubmit Team.
29 changes: 21 additions & 8 deletions README.rst
Expand Up @@ -5,21 +5,31 @@ Django.
Status and License
==================

It is written by Andrey Podoplelov and Evgeny V. Generalov. It is licensed under
an Simplified BSD License.
It is licensed under an Simplified BSD License.


Configuration
=============
Add 'django_resubmit' to the INSTALLED_APPS section in your django settings file.

Also you can setup cache backend for you project in settings.py, for example::

DJANGO_RESUBMIT_BACKEND = "file:///tmp/?timeout=600"

For more information about cache see Django documentation.


Usage
=====

Supply FileFiled with custom FileCacheWidget widget::
Supply FileFiled with custom FileWidget widget::

from django import forms
from django_resubmit.forms.widgets import FileCacheWidget
from django_resubmit.forms.widgets import FileWidget
class SampleForm(forms.Form):
name = forms.CharField(max_length=25)
file = forms.FileField(widget=FileCacheWidget)
file = forms.FileField(widget=FileWidget(thumb_size=[50,50]))


What It Does
Expand All @@ -33,6 +43,12 @@ random key and injects this key as hidden field into the form then
ValidationError is occured. When user resubmits the form It restores the file
from the cache and put it into the ``request.FILES``.

It automatically generates and shows thumbnails for uploaded image files. You
can easily extend it to show video, flash, etc.

It makes Javascript image preview for just selected(not uploaded) files. Works
in Chrome, Firefox and IE.


How To Run Tests
================
Expand All @@ -49,9 +65,6 @@ Use virtualenv::
Bugs and TODO
=============

* Separate FileCacheWidget from django.contrib.admin
* Handle large files (larger than FILE_UPLOAD_MAX_MEMORY_SIZE)
* Add support for ImageField
* Write documentation
* Commit into the Django

Binary file added django_resubmit/fixtures/test-image.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 52 additions & 20 deletions django_resubmit/forms/widgets.py
@@ -1,42 +1,48 @@
# -*- coding: utf-8 -*-
import random
import string
import time
import random

from django.conf import settings
from django.contrib.admin.widgets import AdminFileWidget
from django.forms.fields import FileField
from django.forms.widgets import HiddenInput, CheckboxInput
from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.forms.util import flatatt
from django.utils.safestring import mark_safe
from django.utils.html import escape
from django.utils.encoding import StrAndUnicode, force_unicode
from django.utils.encoding import force_unicode

from django_resubmit.storage import TemporaryFileStorage
from django_resubmit.storage import get_default_storage
from django_resubmit.thumbnails import ThumbFabrica

FILE_INPUT_CLEAR = False


class FileCacheWidget(AdminFileWidget):
class FileWidget(AdminFileWidget):

class Media:
js = (settings.STATIC_URL + 'django_resubmit/jquery.input_image_preview.js',)

def __init__(self, *args, **kwargs):
backend = kwargs.pop("backend", None)
self.storage = TemporaryFileStorage(backend=backend)
"""
thumb_size - preview image size, default [50,50].
"""
self.thumb_size = kwargs.pop("thumb_size", [50, 50])
self.set_storage(get_default_storage())
self.hidden_keyname = u""
self.hidden_key = u""
self.isSaved = False
super(FileCacheWidget, self).__init__(*args, **kwargs)
super(FileWidget, self).__init__(*args, **kwargs)

def value_from_datadict(self, data, files, name):
self.hidden_keyname = "%s-cachekey" % name
self.hidden_key = data.get(self.hidden_keyname, "")

upload = super(FileCacheWidget, self).value_from_datadict(data, files, name)
upload = super(FileWidget, self).value_from_datadict(data, files, name)

# user checks 'clear' or checks 'clear' and uploads a file
if upload is FILE_INPUT_CLEAR or upload is FILE_INPUT_CONTRADICTION:
if self.hidden_key:
self.storage.clear_file(self.hidden_key)
self._storage.clear_file(self.hidden_key)
self.hidden_key = u""
return upload

Expand All @@ -45,10 +51,13 @@ def value_from_datadict(self, data, files, name):
# generate random key
self.hidden_key = string.replace(unicode(random.random() * time.time()), ".", "")
upload = files[name]

upload.file.seek(0)
self.storage.put_file(self.hidden_key, upload)

self._storage.put_file(self.hidden_key, upload)

elif self.hidden_key:
restored = self.storage.get_file(self.hidden_key, name)
restored = self._storage.get_file(self.hidden_key, name)
if restored:
upload = restored
files[name] = upload
Expand All @@ -66,12 +75,7 @@ def render(self, name, value, attrs=None):
}
template = u'%(input)s'

if value is None:
value = ''
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
#if value != '' and self.hidden_key:
# # Only add the 'value' attribute if a value is non-empty.
# final_attrs['value'] = force_unicode(self._format_value(value))
substitutions['input'] = mark_safe(u'<input%s />' % flatatt(final_attrs))

if value and hasattr(value, "url"):
Expand All @@ -92,8 +96,36 @@ def render(self, name, value, attrs=None):
html = template % substitutions

if self.hidden_key:
hi = HiddenInput()
html += hi.render(self.hidden_keyname, self.hidden_key, {})
key_hidden_input = HiddenInput()
html += key_hidden_input.render(self.hidden_keyname, self.hidden_key, {})

if value:
thumb = ThumbFabrica(value, self).thumb()
if thumb:
try:
html += thumb.render()
except:
html += u"Can't create preview"

html += """
<script>
(function($){
$(function(){
$("#id_%s").inputImagePreview({
place: function(frame) { $(this).before(frame); },
imageUrl: function(){ return $(this).prevAll("a:first").attr("href");},
maxWidth: %s,
});
})
})(jQuery || django.jQuery);
</script>
""" % (name, self.thumb_size[0])

return mark_safe(html)

def set_storage(self, value):
self._storage = value




142 changes: 142 additions & 0 deletions django_resubmit/static/django_resubmit/jquery.input_image_preview.js
@@ -0,0 +1,142 @@
/**
* Show image preview for <input type="file"> form field.
*
* $(":file").inputImagePreview({
* place: function(frame) { $(this).before(frame); },
* // <a href="/image.jpg">image.jpg</a>
* // <input type="file">
* // display any url when input is empty
* imageUrl: function(){ return $(this).prevAll("a:first").attr("href");}
* });
*
* @author Evgeny V. Generalov <e.generalov@gmail.com>
*/

(function($){
var pluginName = "inputImagePreview";
var defaultOptions = {
maxWidth: 100,
imageUrl: null,
containerClass: 'image-preview',
imageClass: 'image-preview-picture',
atServerClass: 'atserver',
widthClass: 'width',
heightClass: 'height',
dom: null,
image: null,
url: 'javascript:void(0)'
};
var base64ImgUriPattern = /^data:image\/((png)|(gif)|(jpg)|(jpeg)|(bmp));base64/i;


$.extend($.fn, {
inputImagePreview: function(options) {
return this.each(function(){
var $this = $(this);
if ($this.data(pluginName)) {
var inputImagePreview = $this.data(pluginName);
$.extend(true, inputImagePreview, options);
inputImagePreview.render();
} else {
$this.data(pluginName, new InputImagePreview(this, options));
}
})
}
});

var InputImagePreview = function(elem, options) {
var self = this;

this.inputFile = $(elem);
this.options = $.extend(true, defaultOptions, options);

if (!this.options.dom) {
this.dom = $('<a/>', {'href':options.url, 'target': '_blank',
'class':this.options.domClass}).append(
this.image = $('<img/>', {'class':this.options.imageClass})).append(
this.width = $('<span></span>', {'class':this.options.widthClass}).append('×').append(
this.height = $('<span></span>', {'class':this.options.heightClass})));
this.dom.insertAfter(this.inputFile);
} else {
this.dom = this.options.dom;
this.image = this.options.image || this.dom.find('img');
this.width = this.options.width || this.dom.parent().find('.'+this.options.widthClass);
this.height = this.options.height || this.dom.parent().find('.'+this.options.heightClass);
this.options.url = this.dom.attr('href');
}

this.inputFile.bind({
change: function(){self.render()}
});

this.dom.addClass(this.options.domClass);
this.image.addClass(this.options.imageClass);

this.render();
}

$.extend(InputImagePreview.prototype, {
updateImage: function() {
var inputFileNode = this.inputFile.get(0);
var image = this.image;
var file = inputFileNode.files && inputFileNode.files[0];
var imageUrl= null;
var hasUrl = false;
// Check if we can access the serialized file via getAsDataURL().
if (file) {
/* firefox or chrome */
if (file.getAsDataURL) {
/* firefox */
var src = file.getAsDataURL();
if (src && base64ImgUriPattern.test(src)) {
imageUrl = src;
}
}
} else if (inputFileNode.value) {
/* maybe ie */
imageUrl = this.value;
} else {
/* empty input */
imageUrl = this.options.thumbnailUrl || image.attr('src');
hasUrl = !!imageUrl;
}

if (!imageUrl) {
image.hide()
.removeAttr('src')
;
} else {
image.attr({"src": imageUrl})
.css({'max-width': this.options.maxWidth,
'max-height': this.options.maxHeight})
;
if (hasUrl) {
image.addClass(this.options.atServerClass);
} else {
image.removeClass(this.options.atServerClass);
// adjust width and height
var img = new Image();
img.src=imageUrl;
// we must wait for image loading, therefore we
// take img.width and img.height values assyncroniusly
setTimeout(function(c, img) {
c.width.text(img.width);
c.height.text(img.height)}, 1, this, img);
}
image.show();
}
},
updateContainer: function() {
if (this.image.hasClass(this.options.atServerClass)) {
this.dom.attr({'href': this.options.url});
} else {
this.dom.removeAttr('href');
}
},
render: function() {
this.updateImage();
this.updateContainer();
}
});

})(jQuery || django.jQuery);
12 changes: 7 additions & 5 deletions django_resubmit/storage.py
@@ -1,5 +1,7 @@
# coding: utf-8
from cStringIO import StringIO
from django.conf import settings
from django.core.cache import get_cache
from django.core.files.uploadedfile import InMemoryUploadedFile


Expand All @@ -9,17 +11,17 @@
FIELD_CHARSET = "charset"
FIELD_CONTENT = "content"

def get_default_storage():
backend = getattr(settings, 'DJANGO_RESUBMIT_BACKEND', 'file:///tmp')
return TemporaryFileStorage(backend=get_cache(backend))


class TemporaryFileStorage(object):
def __init__(self, backend=None, prefix=None, max_in_memory_size=None):
def __init__(self, backend=None, prefix=None):
if backend is None:
from django.core.cache import cache as backend
if prefix is None:
prefix = 'cachefile-'
if max_in_memory_size is None:
from django.conf import settings
max_in_memory_size = settings.FILE_UPLOAD_MAX_MEMORY_SIZE
self.max_in_memory_size = max_in_memory_size
self.backend = backend
self.prefix = prefix

Expand Down

0 comments on commit bb7f62b

Please sign in to comment.