├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── ajax_upload ├── __init__.py ├── admin.py ├── forms.py ├── models.py ├── settings.py ├── static │ └── ajax_upload │ │ ├── css │ │ └── ajax-upload-widget.css │ │ └── js │ │ ├── ajax-upload-widget.js │ │ └── jquery.iframe-transport.js ├── tests │ ├── __init__.py │ ├── files │ │ └── test.png │ ├── forms.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── urls.py ├── views.py └── widgets.py ├── example ├── __init__.py ├── forms.py ├── models.py ├── templates │ └── example │ │ └── product.html ├── urls.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Zach Mathew 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Trapeze Media nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include CHANGES 4 | recursive-include ajax_upload/static * 5 | recursive-include ajax_upload/templates * 6 | recursive-include ajax_upload/tests/files * 7 | include example/*.py 8 | recursive-include example/templates * 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **This project is no longer maintained.** 2 | 3 | Django Ajax Upload Widget 4 | ========================= 5 | 6 | Provides AJAX file upload functionality for FileFields and ImageFields with a simple widget replacement in the form. 7 | 8 | No change is required your model fields or app logic. This plugin acts transparently so your model forms can treat files as if they were uploaded by "traditional" browser file upload. 9 | 10 | 11 | Features 12 | -------- 13 | 14 | * Drop-in replacement for Django's built-in ``ClearableFileInput`` widget (no change required to your model). 15 | * Works in all major browsers including IE 7+. 16 | * Random hash string added to file names to ensure uploaded file paths are not guessable by others. 17 | 18 | 19 | Usage 20 | ----- 21 | 22 | Refer to the ``example`` app included in the package for a working example. 23 | 24 | Server Side 25 | ''''''''''' 26 | 27 | In your form, use the ``AjaxClearableFileInput`` on your ``FileField`` or ``ImageField``. 28 | :: 29 | 30 | from django import forms 31 | from ajax_upload.widgets import AjaxClearableFileInput 32 | 33 | class MyForm(forms.Form): 34 | my_image_field = forms.ImageField(widget=AjaxClearableFileInput()) 35 | 36 | 37 | Or, if using a ``ModelForm`` you can just override the widget. 38 | :: 39 | 40 | from django import forms 41 | from ajax_upload.widgets import AjaxClearableFileInput 42 | 43 | class MyForm(forms.ModelForm): 44 | class Meta: 45 | model = MyModel 46 | widgets = { 47 | 'my_image_field': AjaxClearableFileInput 48 | } 49 | 50 | 51 | Client Side 52 | ''''''''''' 53 | 54 | Include the Javascript (and optionally CSS) files in your page and call the ``autoDiscover`` function. 55 | This will search the page for all the AJAX file input fields and apply the necessary Javascript. 56 | :: 57 | 58 | 59 | 60 | 61 | 62 | 67 | 68 | 69 | You can also pass options to ``autoDiscover()``: 70 | :: 71 | 72 | 81 | 82 | 83 | OR ... you can explicitly instantiate an AjaxUploadWidget on an AJAX file input field: 84 | :: 85 | 86 | 87 | 88 | 89 | 94 | 95 | 96 | Dependencies 97 | ------------ 98 | * jQuery 1.7+ 99 | * jQuery Iframe Transport plugin (included in this package) 100 | 101 | 102 | App Installation 103 | ---------------- 104 | 105 | 1. Add ``ajax_upload`` to your ``INSTALLED_APPS`` setting. 106 | 107 | 1. Hook in the urls. 108 | :: 109 | 110 | # urls.py 111 | urlpatterns += patterns('', 112 | (r'^ajax-upload/', include('ajax_upload.urls')), 113 | ) 114 | 115 | 1. That's it (don't forget include the Javascript as mentioned above). 116 | 117 | 118 | Running the Tests 119 | ----------------- 120 | :: 121 | 122 | ./manage.py test ajax_upload 123 | 124 | 125 | License 126 | ------- 127 | 128 | This app is licensed under the BSD license. See the LICENSE file for details. 129 | -------------------------------------------------------------------------------- /ajax_upload/__init__.py: -------------------------------------------------------------------------------- 1 | """Provides AJAX file upload functionality for FileFields and ImageFields with a simple widget replacement in the form.""" 2 | 3 | VERSION = (0, 5, 5) 4 | 5 | __version__ = '.'.join(map(str, VERSION)) 6 | -------------------------------------------------------------------------------- /ajax_upload/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from ajax_upload.models import UploadedFile 4 | 5 | 6 | class UploadedFileAdmin(admin.ModelAdmin): 7 | list_display = ('__unicode__',) 8 | date_hierarchy = 'creation_date' 9 | search_fields = ('file',) 10 | 11 | 12 | admin.site.register(UploadedFile, UploadedFileAdmin) 13 | -------------------------------------------------------------------------------- /ajax_upload/forms.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django import forms 4 | 5 | from ajax_upload.models import UploadedFile 6 | 7 | 8 | class UploadedFileForm(forms.ModelForm): 9 | 10 | class Meta: 11 | model = UploadedFile 12 | fields = ('file',) 13 | 14 | def clean_file(self): 15 | data = self.cleaned_data['file'] 16 | # Change the name of the file to something unguessable 17 | # Construct the new name as -. 18 | data.name = u'%s-%s' % (uuid.uuid4().hex, data.name) 19 | return data 20 | -------------------------------------------------------------------------------- /ajax_upload/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from ajax_upload.settings import FILE_FIELD_MAX_LENGTH, UPLOAD_TO_DIRECTORY 5 | 6 | 7 | class UploadedFile(models.Model): 8 | creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) 9 | file = models.FileField(_('file'), max_length=FILE_FIELD_MAX_LENGTH, upload_to=UPLOAD_TO_DIRECTORY) 10 | 11 | class Meta: 12 | ordering = ('id',) 13 | verbose_name = _('uploaded file') 14 | verbose_name_plural = _('uploaded files') 15 | 16 | def __unicode__(self): 17 | return unicode(self.file) 18 | 19 | def delete(self, *args, **kwargs): 20 | super(UploadedFile, self).delete(*args, **kwargs) 21 | if self.file: 22 | self.file.delete() 23 | delete.alters_data = True 24 | -------------------------------------------------------------------------------- /ajax_upload/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | FILE_FIELD_MAX_LENGTH = getattr(settings, 'AJAX_UPLOAD_FILE_FIELD_MAX_LENGTH', 255) 5 | UPLOAD_TO_DIRECTORY = getattr(settings, 'AJAX_UPLOAD_FILE_TARGET_DIRECTORY', 'ajax_uploads/') 6 | -------------------------------------------------------------------------------- /ajax_upload/static/ajax_upload/css/ajax-upload-widget.css: -------------------------------------------------------------------------------- 1 | .ajax-upload-preview-area { 2 | display: inline-block; 3 | background: #ddd; 4 | border-radius: 6px; 5 | border: 2px dashed #999; 6 | padding: 5px; 7 | margin-right: 5px; 8 | font-size: 12px; 9 | font-family: Arial, sans-serif; 10 | text-align: center; 11 | } 12 | .ajax-upload-preview-area img { 13 | display: block; 14 | max-width: 130px; 15 | max-height: 130px; 16 | border: 1px solid #666; 17 | margin: 0 auto; 18 | margin-top: 5px; 19 | } -------------------------------------------------------------------------------- /ajax_upload/static/ajax_upload/js/ajax-upload-widget.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var global = this; 3 | var $ = global.$; 4 | var console = global.console || {log: function() {}}; 5 | 6 | var AjaxUploadWidget = global.AjaxUploadWidget = function(element, options) { 7 | this.options = { 8 | changeButtonText: 'Change', 9 | removeButtonText: 'Remove', 10 | previewAreaClass: 'ajax-upload-preview-area', 11 | previewFilenameLength: 30, 12 | onUpload: null, // right before uploading to the server 13 | onComplete: null, 14 | onError: null, 15 | onRemove: null 16 | }; 17 | $.extend(this.options, options); 18 | this.$element = $(element); 19 | this.initialize(); 20 | }; 21 | 22 | AjaxUploadWidget.prototype.DjangoAjaxUploadError = function(message) { 23 | this.name = 'DjangoAjaxUploadError'; 24 | this.message = message; 25 | }; 26 | AjaxUploadWidget.prototype.DjangoAjaxUploadError.prototype = new Error(); 27 | AjaxUploadWidget.prototype.DjangoAjaxUploadError.prototype.constructor = AjaxUploadWidget.prototype.DjangoAjaxUploadError; 28 | 29 | AjaxUploadWidget.prototype.initialize = function() { 30 | var self = this; 31 | this.name = this.$element.attr('name'); 32 | 33 | // Create a hidden field to contain our uploaded file name 34 | this.$hiddenElement = $('') 35 | .attr('name', this.name) 36 | .val(this.$element.data('filename')); 37 | this.$element.attr('name', ''); // because we don't want to conflict with our hidden field 38 | this.$element.after(this.$hiddenElement); 39 | 40 | // Initialize preview area and action buttons 41 | this.$previewArea = $('
'); 42 | this.$element.before(this.$previewArea); 43 | 44 | // Listen for when a file is selected, and perform upload 45 | this.$element.on('change', function(evt) { 46 | self.upload(); 47 | }); 48 | this.$changeButton = $('') 49 | .text(this.options.changeButtonText) 50 | .on('click', function(evt) { 51 | self.$element.show(); 52 | $(this).hide(); 53 | }); 54 | this.$element.after(this.$changeButton); 55 | 56 | this.$removeButton = $('') 57 | .text(this.options.removeButtonText) 58 | .on('click', function(evt) { 59 | if(self.options.onRemove) { 60 | var result = self.options.onRemove.call(self); 61 | if(result === false) return; 62 | } 63 | self.$hiddenElement.val(''); 64 | self.displaySelection(); 65 | }); 66 | this.$changeButton.after(this.$removeButton); 67 | 68 | this.displaySelection(); 69 | }; 70 | 71 | AjaxUploadWidget.prototype.upload = function() { 72 | var self = this; 73 | if(!this.$element.val()) return; 74 | if(this.options.onUpload) { 75 | var result = this.options.onUpload.call(this); 76 | if(result === false) return; 77 | } 78 | this.$element.attr('name', 'file'); 79 | $.ajax(this.$element.data('upload-url'), { 80 | iframe: true, 81 | files: this.$element, 82 | processData: false, 83 | type: 'POST', 84 | dataType: 'json', 85 | success: function(data) { self.uploadDone(data); }, 86 | error: function(data) { self.uploadFail(data); } 87 | }); 88 | }; 89 | 90 | AjaxUploadWidget.prototype.uploadDone = function(data) { 91 | // This handles errors as well because iframe transport does not 92 | // distinguish between 200 response and other errors 93 | if(data.errors) { 94 | if(this.options.onError) { 95 | this.options.onError.call(this, data); 96 | } else { 97 | console.log('Upload failed:'); 98 | console.log(data); 99 | } 100 | } else { 101 | this.$hiddenElement.val(data.path); 102 | var tmp = this.$element; 103 | this.$element = this.$element.clone(true).val(''); 104 | tmp.replaceWith(this.$element); 105 | this.displaySelection(); 106 | if(this.options.onComplete) this.options.onComplete.call(this, data.path); 107 | } 108 | }; 109 | 110 | AjaxUploadWidget.prototype.uploadFail = function(xhr) { 111 | if(this.options.onError) { 112 | this.options.onError.call(this); 113 | } else { 114 | console.log('Upload failed:'); 115 | console.log(xhr); 116 | } 117 | }; 118 | 119 | AjaxUploadWidget.prototype.displaySelection = function() { 120 | var filename = this.$hiddenElement.val(); 121 | 122 | if(filename !== '') { 123 | this.$previewArea.empty(); 124 | this.$previewArea.append(this.generateFilePreview(filename)); 125 | 126 | this.$previewArea.show(); 127 | this.$changeButton.show(); 128 | if(this.$element.data('required') === 'True') { 129 | this.$removeButton.hide(); 130 | } else { 131 | this.$removeButton.show(); 132 | } 133 | this.$element.hide(); 134 | } else { 135 | this.$previewArea.slideUp(); 136 | this.$changeButton.hide(); 137 | this.$removeButton.hide(); 138 | this.$element.show(); 139 | } 140 | }; 141 | 142 | AjaxUploadWidget.prototype.generateFilePreview = function(filename) { 143 | // Returns the html output for displaying the given uploaded filename to the user. 144 | var prettyFilename = this.prettifyFilename(filename); 145 | var output = ''+prettyFilename+''; 146 | $.each(['jpg', 'jpeg', 'png', 'gif'], function(i, ext) { 147 | if(filename.toLowerCase().slice(-ext.length) == ext) { 148 | output += ''; 149 | return false; 150 | } 151 | }); 152 | output += ''; 153 | return output; 154 | }; 155 | 156 | AjaxUploadWidget.prototype.prettifyFilename = function(filename) { 157 | // Get rid of the folder names 158 | var cleaned = filename.slice(filename.lastIndexOf('/')+1); 159 | 160 | // Strip the random hex in the filename inserted by the backend (if present) 161 | var re = /^[a-f0-9]{32}\-/i; 162 | cleaned = cleaned.replace(re, ''); 163 | 164 | // Truncate the filename 165 | var maxChars = this.options.previewFilenameLength; 166 | var elipsis = '...'; 167 | if(cleaned.length > maxChars) { 168 | cleaned = elipsis + cleaned.slice((-1 * maxChars) + elipsis.length); 169 | } 170 | return cleaned; 171 | }; 172 | 173 | AjaxUploadWidget.autoDiscover = function(options) { 174 | $('input[type="file"].ajax-upload').each(function(index, element) { 175 | new AjaxUploadWidget(element, options); 176 | }); 177 | }; 178 | }).call(this); 179 | -------------------------------------------------------------------------------- /ajax_upload/static/ajax_upload/js/jquery.iframe-transport.js: -------------------------------------------------------------------------------- 1 | // This [jQuery](http://jquery.com/) plugin implements an `"); 171 | 172 | // The first load event gets fired after the iframe has been injected 173 | // into the DOM, and is used to prepare the actual submission. 174 | iframe.bind("load", function() { 175 | 176 | // The second load event gets fired when the response to the form 177 | // submission is received. The implementation detects whether the 178 | // actual payload is embedded in a `