├── .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 `