├── .gitignore ├── README.md ├── __init__.py ├── manage.py ├── settings.py ├── static ├── css │ └── jquery.fileupload-ui.css └── js │ ├── jquery.fileupload-ui.js │ ├── jquery.fileupload.js │ ├── jquery.iframe-transport.js │ └── jquery_fix_csrf.js ├── templates └── upload.html ├── upload ├── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py └── urls.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /tmp -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django-jQuery-File-Uploader-Integration-demo 2 | ============================================ 3 | 4 | Please note that this demo by now is outdated however might 5 | provide some help to newcomers hence I'm not deleting it. 6 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miki725/Django-jQuery-File-Uploader-Integration-demo/98ebf63caebeeb9c78c0207eb553dd3c8320087f/__init__.py -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | import imp 4 | try: 5 | imp.find_module('settings') # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 9 | sys.exit(1) 10 | 11 | import settings 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for djangoUpload project. 2 | 3 | import os 4 | PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | DEBUG = True 7 | TEMPLATE_DEBUG = DEBUG 8 | 9 | ADMINS = ( 10 | # ('Your Name', 'your_email@example.com'), 11 | ('Miroslav Shubernetskiy', 'miroslav@miki725.com'), 12 | ) 13 | 14 | MANAGERS = ADMINS 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 19 | 'NAME': 'djangoupload.db', # Or path to database file if using sqlite3. 20 | 'USER': '', # Not used with sqlite3. 21 | 'PASSWORD': '', # Not used with sqlite3. 22 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 23 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 24 | } 25 | } 26 | 27 | # Local time zone for this installation. Choices can be found here: 28 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 29 | # although not all choices may be available on all operating systems. 30 | # On Unix systems, a value of None will cause Django to use the same 31 | # timezone as the operating system. 32 | # If running in a Windows environment this must be set to the same as your 33 | # system time zone. 34 | TIME_ZONE = 'America/New_York' 35 | 36 | # Language code for this installation. All choices can be found here: 37 | # http://www.i18nguy.com/unicode/language-identifiers.html 38 | LANGUAGE_CODE = 'en-us' 39 | 40 | SITE_ID = 1 41 | 42 | # If you set this to False, Django will make some optimizations so as not 43 | # to load the internationalization machinery. 44 | USE_I18N = True 45 | 46 | # If you set this to False, Django will not format dates, numbers and 47 | # calendars according to the current locale 48 | USE_L10N = True 49 | 50 | # Absolute filesystem path to the directory that will hold user-uploaded files. 51 | # Example: "/home/media/media.lawrence.com/media/" 52 | MEDIA_ROOT = '' 53 | 54 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 55 | # trailing slash. 56 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 57 | MEDIA_URL = '' 58 | 59 | # Absolute path to the directory static files should be collected to. 60 | # Don't put anything in this directory yourself; store your static files 61 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 62 | # Example: "/home/media/media.lawrence.com/static/" 63 | STATIC_ROOT = '' 64 | 65 | # URL prefix for static files. 66 | # Example: "http://media.lawrence.com/static/" 67 | STATIC_URL = '/static/' 68 | 69 | # URL prefix for admin static files -- CSS, JavaScript and images. 70 | # Make sure to use a trailing slash. 71 | # Examples: "http://foo.com/static/admin/", "/static/admin/". 72 | ADMIN_MEDIA_PREFIX = '/static/admin/' 73 | 74 | # Additional locations of static files 75 | STATICFILES_DIRS = ( 76 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 77 | # Always use forward slashes, even on Windows. 78 | # Don't forget to use absolute paths, not relative paths. 79 | os.path.join(PROJECT_ROOT, "static"), 80 | 81 | ) 82 | 83 | # List of finder classes that know how to find static files in 84 | # various locations. 85 | STATICFILES_FINDERS = ( 86 | 'django.contrib.staticfiles.finders.FileSystemFinder', 87 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 88 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 89 | ) 90 | 91 | # Make this unique, and don't share it with anybody. 92 | # So this file is on github so I guess the secret is out!!! 93 | SECRET_KEY = 'gcs^9v-hbl9qwavnbn&@e794ir@tyrrnz(+0efshm!dzzo_xt+' 94 | 95 | # List of callables that know how to import templates from various sources. 96 | TEMPLATE_LOADERS = ( 97 | 'django.template.loaders.filesystem.Loader', 98 | 'django.template.loaders.app_directories.Loader', 99 | # 'django.template.loaders.eggs.Loader', 100 | ) 101 | 102 | MIDDLEWARE_CLASSES = ( 103 | 'django.middleware.common.CommonMiddleware', 104 | 'django.contrib.sessions.middleware.SessionMiddleware', 105 | 'django.middleware.csrf.CsrfViewMiddleware', 106 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 107 | 'django.contrib.messages.middleware.MessageMiddleware', 108 | ) 109 | 110 | ROOT_URLCONF = 'djangoUpload.urls' 111 | 112 | TEMPLATE_DIRS = ( 113 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 114 | # Always use forward slashes, even on Windows. 115 | # Don't forget to use absolute paths, not relative paths. 116 | os.path.join(PROJECT_ROOT, "templates") 117 | ) 118 | 119 | INSTALLED_APPS = ( 120 | 'django.contrib.auth', 121 | 'django.contrib.contenttypes', 122 | 'django.contrib.sessions', 123 | 'django.contrib.sites', 124 | 'django.contrib.messages', 125 | 'django.contrib.staticfiles', 126 | # Uncomment the next line to enable the admin: 127 | # 'django.contrib.admin', 128 | # Uncomment the next line to enable admin documentation: 129 | # 'django.contrib.admindocs', 130 | ) 131 | 132 | # A sample logging configuration. The only tangible logging 133 | # performed by this configuration is to send an email to 134 | # the site admins on every HTTP 500 error. 135 | # See http://docs.djangoproject.com/en/dev/topics/logging for 136 | # more details on how to customize your logging configuration. 137 | LOGGING = { 138 | 'version': 1, 139 | 'disable_existing_loggers': False, 140 | 'handlers': { 141 | 'mail_admins': { 142 | 'level': 'ERROR', 143 | 'class': 'django.utils.log.AdminEmailHandler' 144 | } 145 | }, 146 | 'loggers': { 147 | 'django.request': { 148 | 'handlers': ['mail_admins'], 149 | 'level': 'ERROR', 150 | 'propagate': True, 151 | }, 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /static/css/jquery.fileupload-ui.css: -------------------------------------------------------------------------------- 1 | @charset 'UTF-8'; 2 | /* 3 | * jQuery File Upload UI Plugin CSS 5.0.6 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2010, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * http://creativecommons.org/licenses/MIT/ 11 | */ 12 | 13 | .fileupload-buttonbar .ui-button input { 14 | position: absolute; 15 | top: 0; 16 | right: 0; 17 | margin: 0; 18 | border: solid transparent; 19 | border-width: 0 0 100px 200px; 20 | opacity: 0; 21 | filter: alpha(opacity=0); 22 | -o-transform: translate(250px, -50px) scale(1); 23 | -moz-transform: translate(-300px, 0) scale(4); 24 | direction: ltr; 25 | cursor: pointer; 26 | } 27 | 28 | .fileinput-button { 29 | overflow: hidden; 30 | } 31 | 32 | /* Fix for IE 6: */ 33 | *html .fileinput-button { 34 | padding: 2px 0; 35 | } 36 | 37 | /* Fix for IE 7: */ 38 | *+html .fileinput-button { 39 | padding: 2px 0; 40 | } 41 | 42 | .fileupload-buttonbar { 43 | padding: 0.2em 0.4em; 44 | } 45 | 46 | .fileupload-buttonbar .ui-button { 47 | vertical-align: middle; 48 | } 49 | 50 | .fileupload-content { 51 | padding: 0.2em 0.4em; 52 | border-top-width: 0; 53 | } 54 | 55 | .fileupload-content .ui-progressbar { 56 | width: 200px; 57 | height: 20px; 58 | } 59 | 60 | .fileupload-content .ui-progressbar-value { 61 | background: url(pbar-ani.gif); 62 | } 63 | 64 | .fileupload-content .fileupload-progressbar { 65 | width: 400px; 66 | margin: 10px 0; 67 | } 68 | 69 | .files { 70 | margin: 10px 0; 71 | border-collapse: collapse; 72 | } 73 | 74 | .files td { 75 | padding: 5px; 76 | border-spacing: 5px; 77 | } 78 | 79 | .files img { 80 | border: none; 81 | } 82 | 83 | .files .name { 84 | padding: 0 10px; 85 | } 86 | 87 | .files .size { 88 | padding: 0 10px 0 0; 89 | text-align: right; 90 | white-space: nowrap; 91 | } 92 | 93 | .ui-state-disabled .ui-state-disabled { 94 | opacity: 1; 95 | filter: alpha(opacity=100); 96 | } 97 | 98 | .ui-state-disabled input { 99 | cursor: default; 100 | } -------------------------------------------------------------------------------- /static/js/jquery.fileupload-ui.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload User Interface Plugin 5.0.13 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2010, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://creativecommons.org/licenses/MIT/ 10 | */ 11 | 12 | /*jslint nomen: true, unparam: true, regexp: true */ 13 | /*global window, document, URL, webkitURL, FileReader, jQuery */ 14 | 15 | (function ($) { 16 | 'use strict'; 17 | 18 | // The UI version extends the basic fileupload widget and adds 19 | // a complete user interface based on the given upload/download 20 | // templates. 21 | $.widget('blueimpUI.fileupload', $.blueimp.fileupload, { 22 | 23 | options: { 24 | // By default, files added to the widget are uploaded as soon 25 | // as the user clicks on the start buttons. To enable automatic 26 | // uploads, set the following option to true: 27 | autoUpload: false, 28 | // The following option limits the number of files that are 29 | // allowed to be uploaded using this widget: 30 | maxNumberOfFiles: undefined, 31 | // The maximum allowed file size: 32 | maxFileSize: undefined, 33 | // The minimum allowed file size: 34 | minFileSize: 1, 35 | // The regular expression for allowed file types, matches 36 | // against either file type or file name: 37 | acceptFileTypes: /.+$/i, 38 | // The regular expression to define for which files a preview 39 | // image is shown, matched against the file type: 40 | previewFileTypes: /^image\/(gif|jpeg|png)$/, 41 | // The maximum width of the preview images: 42 | previewMaxWidth: 80, 43 | // The maximum height of the preview images: 44 | previewMaxHeight: 80, 45 | // By default, preview images are displayed as canvas elements 46 | // if supported by the browser. Set the following option to false 47 | // to always display preview images as img elements: 48 | previewAsCanvas: true, 49 | // The file upload template that is given as first argument to the 50 | // jQuery.tmpl method to render the file uploads: 51 | uploadTemplate: $('#template-upload'), 52 | // The file download template, that is given as first argument to the 53 | // jQuery.tmpl method to render the file downloads: 54 | downloadTemplate: $('#template-download'), 55 | // The expected data type of the upload response, sets the dataType 56 | // option of the $.ajax upload requests: 57 | dataType: 'json', 58 | 59 | // The add callback is invoked as soon as files are added to the fileupload 60 | // widget (via file input selection, drag & drop or add API call). 61 | // See the basic file upload widget for more information: 62 | add: function (e, data) { 63 | var that = $(this).data('fileupload'); 64 | that._adjustMaxNumberOfFiles(-data.files.length); 65 | data.isAdjusted = true; 66 | data.isValidated = that._validate(data.files); 67 | data.context = that._renderUpload(data.files) 68 | .appendTo($(this).find('.files')).fadeIn(function () { 69 | // Fix for IE7 and lower: 70 | $(this).show(); 71 | }).data('data', data); 72 | if ((that.options.autoUpload || data.autoUpload) && 73 | data.isValidated) { 74 | data.jqXHR = data.submit(); 75 | } 76 | }, 77 | // Callback for the start of each file upload request: 78 | send: function (e, data) { 79 | if (!data.isValidated) { 80 | var that = $(this).data('fileupload'); 81 | if (!data.isAdjusted) { 82 | that._adjustMaxNumberOfFiles(-data.files.length); 83 | } 84 | if (!that._validate(data.files)) { 85 | return false; 86 | } 87 | } 88 | if (data.context && data.dataType && 89 | data.dataType.substr(0, 6) === 'iframe') { 90 | // Iframe Transport does not support progress events. 91 | // In lack of an indeterminate progress bar, we set 92 | // the progress to 100%, showing the full animated bar: 93 | data.context.find('.ui-progressbar').progressbar( 94 | 'value', 95 | parseInt(100, 10) 96 | ); 97 | } 98 | }, 99 | // Callback for successful uploads: 100 | done: function (e, data) { 101 | var that = $(this).data('fileupload'); 102 | if (data.context) { 103 | data.context.each(function (index) { 104 | var file = ($.isArray(data.result) && 105 | data.result[index]) || {error: 'emptyResult'}; 106 | if (file.error) { 107 | that._adjustMaxNumberOfFiles(1); 108 | } 109 | $(this).fadeOut(function () { 110 | that._renderDownload([file]) 111 | .css('display', 'none') 112 | .replaceAll(this) 113 | .fadeIn(function () { 114 | // Fix for IE7 and lower: 115 | $(this).show(); 116 | }); 117 | }); 118 | }); 119 | } else { 120 | that._renderDownload(data.result) 121 | .css('display', 'none') 122 | .appendTo($(this).find('.files')) 123 | .fadeIn(function () { 124 | // Fix for IE7 and lower: 125 | $(this).show(); 126 | }); 127 | } 128 | }, 129 | // Callback for failed (abort or error) uploads: 130 | fail: function (e, data) { 131 | var that = $(this).data('fileupload'); 132 | that._adjustMaxNumberOfFiles(data.files.length); 133 | if (data.context) { 134 | data.context.each(function (index) { 135 | $(this).fadeOut(function () { 136 | if (data.errorThrown !== 'abort') { 137 | var file = data.files[index]; 138 | file.error = file.error || data.errorThrown 139 | || true; 140 | that._renderDownload([file]) 141 | .css('display', 'none') 142 | .replaceAll(this) 143 | .fadeIn(function () { 144 | // Fix for IE7 and lower: 145 | $(this).show(); 146 | }); 147 | } else { 148 | data.context.remove(); 149 | } 150 | }); 151 | }); 152 | } else if (data.errorThrown !== 'abort') { 153 | that._adjustMaxNumberOfFiles(-data.files.length); 154 | data.context = that._renderUpload(data.files) 155 | .css('display', 'none') 156 | .appendTo($(this).find('.files')) 157 | .fadeIn(function () { 158 | // Fix for IE7 and lower: 159 | $(this).show(); 160 | }).data('data', data); 161 | } 162 | }, 163 | // Callback for upload progress events: 164 | progress: function (e, data) { 165 | if (data.context) { 166 | data.context.find('.ui-progressbar').progressbar( 167 | 'value', 168 | parseInt(data.loaded / data.total * 100, 10) 169 | ); 170 | } 171 | }, 172 | // Callback for global upload progress events: 173 | progressall: function (e, data) { 174 | $(this).find('.fileupload-progressbar').progressbar( 175 | 'value', 176 | parseInt(data.loaded / data.total * 100, 10) 177 | ); 178 | }, 179 | // Callback for uploads start, equivalent to the global ajaxStart event: 180 | start: function () { 181 | $(this).find('.fileupload-progressbar') 182 | .progressbar('value', 0).fadeIn(); 183 | }, 184 | // Callback for uploads stop, equivalent to the global ajaxStop event: 185 | stop: function () { 186 | $(this).find('.fileupload-progressbar').fadeOut(); 187 | }, 188 | // Callback for file deletion: 189 | destroy: function (e, data) { 190 | var that = $(this).data('fileupload'); 191 | if (data.url) { 192 | $.ajax(data) 193 | .success(function () { 194 | that._adjustMaxNumberOfFiles(1); 195 | $(this).fadeOut(function () { 196 | $(this).remove(); 197 | }); 198 | }); 199 | } else { 200 | data.context.fadeOut(function () { 201 | $(this).remove(); 202 | }); 203 | } 204 | } 205 | }, 206 | 207 | // Scales the given image (img HTML element) 208 | // using the given options. 209 | // Returns a canvas object if the canvas option is true 210 | // and the browser supports canvas, else the scaled image: 211 | _scaleImage: function (img, options) { 212 | options = options || {}; 213 | var canvas = document.createElement('canvas'), 214 | scale = Math.min( 215 | (options.maxWidth || img.width) / img.width, 216 | (options.maxHeight || img.height) / img.height 217 | ); 218 | if (scale >= 1) { 219 | scale = Math.max( 220 | (options.minWidth || img.width) / img.width, 221 | (options.minHeight || img.height) / img.height 222 | ); 223 | } 224 | img.width = parseInt(img.width * scale, 10); 225 | img.height = parseInt(img.height * scale, 10); 226 | if (!options.canvas || !canvas.getContext) { 227 | return img; 228 | } 229 | canvas.width = img.width; 230 | canvas.height = img.height; 231 | canvas.getContext('2d') 232 | .drawImage(img, 0, 0, img.width, img.height); 233 | return canvas; 234 | }, 235 | 236 | _createObjectURL: function (file) { 237 | var undef = 'undefined', 238 | urlAPI = (typeof window.createObjectURL !== undef && window) || 239 | (typeof URL !== undef && URL) || 240 | (typeof webkitURL !== undef && webkitURL); 241 | return urlAPI ? urlAPI.createObjectURL(file) : false; 242 | }, 243 | 244 | _revokeObjectURL: function (url) { 245 | var undef = 'undefined', 246 | urlAPI = (typeof window.revokeObjectURL !== undef && window) || 247 | (typeof URL !== undef && URL) || 248 | (typeof webkitURL !== undef && webkitURL); 249 | return urlAPI ? urlAPI.revokeObjectURL(url) : false; 250 | }, 251 | 252 | // Loads a given File object via FileReader interface, 253 | // invokes the callback with a data url: 254 | _loadFile: function (file, callback) { 255 | if (typeof FileReader !== 'undefined' && 256 | FileReader.prototype.readAsDataURL) { 257 | var fileReader = new FileReader(); 258 | fileReader.onload = function (e) { 259 | callback(e.target.result); 260 | }; 261 | fileReader.readAsDataURL(file); 262 | return true; 263 | } 264 | return false; 265 | }, 266 | 267 | // Loads an image for a given File object. 268 | // Invokes the callback with an img or optional canvas 269 | // element (if supported by the browser) as parameter: 270 | _loadImage: function (file, callback, options) { 271 | var that = this, 272 | url, 273 | img; 274 | if (!options || !options.fileTypes || 275 | options.fileTypes.test(file.type)) { 276 | url = this._createObjectURL(file); 277 | img = $('').bind('load', function () { 278 | $(this).unbind('load'); 279 | that._revokeObjectURL(url); 280 | callback(that._scaleImage(img[0], options)); 281 | }).prop('src', url); 282 | if (!url) { 283 | this._loadFile(file, function (url) { 284 | img.prop('src', url); 285 | }); 286 | } 287 | } 288 | }, 289 | 290 | // Link handler, that allows to download files 291 | // by drag & drop of the links to the desktop: 292 | _enableDragToDesktop: function () { 293 | var link = $(this), 294 | url = link.prop('href'), 295 | name = decodeURIComponent(url.split('/').pop()) 296 | .replace(/:/g, '-'), 297 | type = 'application/octet-stream'; 298 | link.bind('dragstart', function (e) { 299 | try { 300 | e.originalEvent.dataTransfer.setData( 301 | 'DownloadURL', 302 | [type, name, url].join(':') 303 | ); 304 | } catch (err) {} 305 | }); 306 | }, 307 | 308 | _adjustMaxNumberOfFiles: function (operand) { 309 | if (typeof this.options.maxNumberOfFiles === 'number') { 310 | this.options.maxNumberOfFiles += operand; 311 | if (this.options.maxNumberOfFiles < 1) { 312 | this._disableFileInputButton(); 313 | } else { 314 | this._enableFileInputButton(); 315 | } 316 | } 317 | }, 318 | 319 | _formatFileSize: function (file) { 320 | if (typeof file.size !== 'number') { 321 | return ''; 322 | } 323 | if (file.size >= 1000000000) { 324 | return (file.size / 1000000000).toFixed(2) + ' GB'; 325 | } 326 | if (file.size >= 1000000) { 327 | return (file.size / 1000000).toFixed(2) + ' MB'; 328 | } 329 | return (file.size / 1000).toFixed(2) + ' KB'; 330 | }, 331 | 332 | _hasError: function (file) { 333 | if (file.error) { 334 | return file.error; 335 | } 336 | // The number of added files is subtracted from 337 | // maxNumberOfFiles before validation, so we check if 338 | // maxNumberOfFiles is below 0 (instead of below 1): 339 | if (this.options.maxNumberOfFiles < 0) { 340 | return 'maxNumberOfFiles'; 341 | } 342 | // Files are accepted if either the file type or the file name 343 | // matches against the acceptFileTypes regular expression, as 344 | // only browsers with support for the File API report the type: 345 | if (!(this.options.acceptFileTypes.test(file.type) || 346 | this.options.acceptFileTypes.test(file.name))) { 347 | return 'acceptFileTypes'; 348 | } 349 | if (this.options.maxFileSize && 350 | file.size > this.options.maxFileSize) { 351 | return 'maxFileSize'; 352 | } 353 | if (typeof file.size === 'number' && 354 | file.size < this.options.minFileSize) { 355 | return 'minFileSize'; 356 | } 357 | return null; 358 | }, 359 | 360 | _validate: function (files) { 361 | var that = this, 362 | valid; 363 | $.each(files, function (index, file) { 364 | file.error = that._hasError(file); 365 | valid = !file.error; 366 | }); 367 | return valid; 368 | }, 369 | 370 | _uploadTemplateHelper: function (file) { 371 | file.sizef = this._formatFileSize(file); 372 | return file; 373 | }, 374 | 375 | _renderUploadTemplate: function (files) { 376 | var that = this; 377 | return $.tmpl( 378 | this.options.uploadTemplate, 379 | $.map(files, function (file) { 380 | return that._uploadTemplateHelper(file); 381 | }) 382 | ); 383 | }, 384 | 385 | _renderUpload: function (files) { 386 | var that = this, 387 | options = this.options, 388 | tmpl = this._renderUploadTemplate(files); 389 | if (!(tmpl instanceof $)) { 390 | return $(); 391 | } 392 | tmpl.css('display', 'none'); 393 | // .slice(1).remove().end().first() removes all but the first 394 | // element and selects only the first for the jQuery collection: 395 | tmpl.find('.progress div').slice(1).remove().end().first() 396 | .progressbar(); 397 | tmpl.find('.start button').slice( 398 | this.options.autoUpload ? 0 : 1 399 | ).remove().end().first() 400 | .button({ 401 | text: false, 402 | icons: {primary: 'ui-icon-circle-arrow-e'} 403 | }); 404 | tmpl.find('.cancel button').slice(1).remove().end().first() 405 | .button({ 406 | text: false, 407 | icons: {primary: 'ui-icon-cancel'} 408 | }); 409 | tmpl.find('.preview').each(function (index, node) { 410 | that._loadImage( 411 | files[index], 412 | function (img) { 413 | $(img).hide().appendTo(node).fadeIn(); 414 | }, 415 | { 416 | maxWidth: options.previewMaxWidth, 417 | maxHeight: options.previewMaxHeight, 418 | fileTypes: options.previewFileTypes, 419 | canvas: options.previewAsCanvas 420 | } 421 | ); 422 | }); 423 | return tmpl; 424 | }, 425 | 426 | _downloadTemplateHelper: function (file) { 427 | file.sizef = this._formatFileSize(file); 428 | return file; 429 | }, 430 | 431 | _renderDownloadTemplate: function (files) { 432 | var that = this; 433 | return $.tmpl( 434 | this.options.downloadTemplate, 435 | $.map(files, function (file) { 436 | return that._downloadTemplateHelper(file); 437 | }) 438 | ); 439 | }, 440 | 441 | _renderDownload: function (files) { 442 | var tmpl = this._renderDownloadTemplate(files); 443 | if (!(tmpl instanceof $)) { 444 | return $(); 445 | } 446 | tmpl.css('display', 'none'); 447 | tmpl.find('.delete button').button({ 448 | text: false, 449 | icons: {primary: 'ui-icon-trash'} 450 | }); 451 | tmpl.find('a').each(this._enableDragToDesktop); 452 | return tmpl; 453 | }, 454 | 455 | _startHandler: function (e) { 456 | e.preventDefault(); 457 | var tmpl = $(this).closest('.template-upload'), 458 | data = tmpl.data('data'); 459 | if (data && data.submit && !data.jqXHR) { 460 | data.jqXHR = data.submit(); 461 | $(this).fadeOut(); 462 | } 463 | }, 464 | 465 | _cancelHandler: function (e) { 466 | e.preventDefault(); 467 | var tmpl = $(this).closest('.template-upload'), 468 | data = tmpl.data('data') || {}; 469 | if (!data.jqXHR) { 470 | data.errorThrown = 'abort'; 471 | e.data.fileupload._trigger('fail', e, data); 472 | } else { 473 | data.jqXHR.abort(); 474 | } 475 | }, 476 | 477 | _deleteHandler: function (e) { 478 | e.preventDefault(); 479 | var button = $(this); 480 | e.data.fileupload._trigger('destroy', e, { 481 | context: button.closest('.template-download'), 482 | url: button.attr('data-url'), 483 | type: button.attr('data-type'), 484 | dataType: e.data.fileupload.options.dataType 485 | }); 486 | }, 487 | 488 | _initEventHandlers: function () { 489 | $.blueimp.fileupload.prototype._initEventHandlers.call(this); 490 | var filesList = this.element.find('.files'), 491 | eventData = {fileupload: this}; 492 | filesList.find('.start button') 493 | .live( 494 | 'click.' + this.options.namespace, 495 | eventData, 496 | this._startHandler 497 | ); 498 | filesList.find('.cancel button') 499 | .live( 500 | 'click.' + this.options.namespace, 501 | eventData, 502 | this._cancelHandler 503 | ); 504 | filesList.find('.delete button') 505 | .live( 506 | 'click.' + this.options.namespace, 507 | eventData, 508 | this._deleteHandler 509 | ); 510 | }, 511 | 512 | _destroyEventHandlers: function () { 513 | var filesList = this.element.find('.files'); 514 | filesList.find('.start button') 515 | .die('click.' + this.options.namespace); 516 | filesList.find('.cancel button') 517 | .die('click.' + this.options.namespace); 518 | filesList.find('.delete button') 519 | .die('click.' + this.options.namespace); 520 | $.blueimp.fileupload.prototype._destroyEventHandlers.call(this); 521 | }, 522 | 523 | _initFileUploadButtonBar: function () { 524 | var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), 525 | filesList = this.element.find('.files'), 526 | ns = this.options.namespace; 527 | fileUploadButtonBar 528 | .addClass('ui-widget-header ui-corner-top'); 529 | this.element.find('.fileinput-button').each(function () { 530 | var fileInput = $(this).find('input:file').detach(); 531 | $(this).button({icons: {primary: 'ui-icon-plusthick'}}) 532 | .append(fileInput); 533 | }); 534 | fileUploadButtonBar.find('.start') 535 | .button({icons: {primary: 'ui-icon-circle-arrow-e'}}) 536 | .bind('click.' + ns, function (e) { 537 | e.preventDefault(); 538 | filesList.find('.start button').click(); 539 | }); 540 | fileUploadButtonBar.find('.cancel') 541 | .button({icons: {primary: 'ui-icon-cancel'}}) 542 | .bind('click.' + ns, function (e) { 543 | e.preventDefault(); 544 | filesList.find('.cancel button').click(); 545 | }); 546 | fileUploadButtonBar.find('.delete') 547 | .button({icons: {primary: 'ui-icon-trash'}}) 548 | .bind('click.' + ns, function (e) { 549 | e.preventDefault(); 550 | filesList.find('.delete button').click(); 551 | }); 552 | }, 553 | 554 | _destroyFileUploadButtonBar: function () { 555 | this.element.find('.fileupload-buttonbar') 556 | .removeClass('ui-widget-header ui-corner-top'); 557 | this.element.find('.fileinput-button').each(function () { 558 | var fileInput = $(this).find('input:file').detach(); 559 | $(this).button('destroy') 560 | .append(fileInput); 561 | }); 562 | this.element.find('.fileupload-buttonbar button') 563 | .unbind('click.' + this.options.namespace) 564 | .button('destroy'); 565 | }, 566 | 567 | _enableFileInputButton: function () { 568 | this.element.find('.fileinput-button input:file:disabled') 569 | .each(function () { 570 | var fileInput = $(this), 571 | button = fileInput.parent(); 572 | fileInput.detach().prop('disabled', false); 573 | button.button('enable').append(fileInput); 574 | }); 575 | }, 576 | 577 | _disableFileInputButton: function () { 578 | this.element.find('.fileinput-button input:file:enabled') 579 | .each(function () { 580 | var fileInput = $(this), 581 | button = fileInput.parent(); 582 | fileInput.detach().prop('disabled', true); 583 | button.button('disable').append(fileInput); 584 | }); 585 | }, 586 | 587 | _initTemplates: function () { 588 | // Handle cases where the templates are defined 589 | // after the widget library has been included: 590 | if (this.options.uploadTemplate instanceof $ && 591 | !this.options.uploadTemplate.length) { 592 | this.options.uploadTemplate = $( 593 | this.options.uploadTemplate.selector 594 | ); 595 | } 596 | if (this.options.downloadTemplate instanceof $ && 597 | !this.options.downloadTemplate.length) { 598 | this.options.downloadTemplate = $( 599 | this.options.downloadTemplate.selector 600 | ); 601 | } 602 | }, 603 | 604 | _create: function () { 605 | $.blueimp.fileupload.prototype._create.call(this); 606 | this._initTemplates(); 607 | this.element 608 | .addClass('ui-widget'); 609 | this._initFileUploadButtonBar(); 610 | this.element.find('.fileupload-content') 611 | .addClass('ui-widget-content ui-corner-bottom'); 612 | this.element.find('.fileupload-progressbar') 613 | .hide().progressbar(); 614 | }, 615 | 616 | destroy: function () { 617 | this.element.find('.fileupload-progressbar') 618 | .progressbar('destroy'); 619 | this.element.find('.fileupload-content') 620 | .removeClass('ui-widget-content ui-corner-bottom'); 621 | this._destroyFileUploadButtonBar(); 622 | this.element.removeClass('ui-widget'); 623 | $.blueimp.fileupload.prototype.destroy.call(this); 624 | }, 625 | 626 | enable: function () { 627 | $.blueimp.fileupload.prototype.enable.call(this); 628 | this.element.find(':ui-button').not('.fileinput-button') 629 | .button('enable'); 630 | this._enableFileInputButton(); 631 | }, 632 | 633 | disable: function () { 634 | this.element.find(':ui-button').not('.fileinput-button') 635 | .button('disable'); 636 | this._disableFileInputButton(); 637 | $.blueimp.fileupload.prototype.disable.call(this); 638 | } 639 | 640 | }); 641 | 642 | }(jQuery)); -------------------------------------------------------------------------------- /static/js/jquery.fileupload.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Plugin 5.0.3 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2010, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://creativecommons.org/licenses/MIT/ 10 | */ 11 | 12 | /*jslint nomen: true, unparam: true, regexp: true */ 13 | /*global document, XMLHttpRequestUpload, Blob, File, FormData, location, jQuery */ 14 | 15 | (function ($) { 16 | 'use strict'; 17 | 18 | // The fileupload widget listens for change events on file input fields 19 | // defined via fileInput setting and drop events of the given dropZone. 20 | // In addition to the default jQuery Widget methods, the fileupload widget 21 | // exposes the "add" and "send" methods, to add or directly send files 22 | // using the fileupload API. 23 | // By default, files added via file input selection, drag & drop or 24 | // "add" method are uploaded immediately, but it is possible to override 25 | // the "add" callback option to queue file uploads. 26 | $.widget('blueimp.fileupload', { 27 | 28 | options: { 29 | // The namespace used for event handler binding on the dropZone and 30 | // fileInput collections. 31 | // If not set, the name of the widget ("fileupload") is used. 32 | namespace: undefined, 33 | // The drop target collection, by the default the complete document. 34 | // Set to null or an empty collection to disable drag & drop support: 35 | dropZone: $(document), 36 | // The file input field collection, that is listened for change events. 37 | // If undefined, it is set to the file input fields inside 38 | // of the widget element on plugin initialization. 39 | // Set to null or an empty collection to disable the change listener. 40 | fileInput: undefined, 41 | // By default, the file input field is replaced with a clone after 42 | // each input field change event. This is required for iframe transport 43 | // queues and allows change events to be fired for the same file 44 | // selection, but can be disabled by setting the following option to false: 45 | replaceFileInput: true, 46 | // The parameter name for the file form data (the request argument name). 47 | // If undefined or empty, the name property of the file input field is 48 | // used, or "files[]" if the file input name property is also empty: 49 | paramName: undefined, 50 | // By default, each file of a selection is uploaded using an individual 51 | // request for XHR type uploads. Set to false to upload file 52 | // selections in one request each: 53 | singleFileUploads: true, 54 | // Set the following option to true to issue all file upload requests 55 | // in a sequential order: 56 | sequentialUploads: false, 57 | // Set the following option to true to force iframe transport uploads: 58 | forceIframeTransport: false, 59 | // By default, XHR file uploads are sent as multipart/form-data. 60 | // The iframe transport is always using multipart/form-data. 61 | // Set to false to enable non-multipart XHR uploads: 62 | multipart: true, 63 | // To upload large files in smaller chunks, set the following option 64 | // to a preferred maximum chunk size. If set to 0, null or undefined, 65 | // or the browser does not support the required Blob API, files will 66 | // be uploaded as a whole. 67 | maxChunkSize: undefined, 68 | // When a non-multipart upload or a chunked multipart upload has been 69 | // aborted, this option can be used to resume the upload by setting 70 | // it to the size of the already uploaded bytes. This option is most 71 | // useful when modifying the options object inside of the "add" or 72 | // "send" callbacks, as the options are cloned for each file upload. 73 | uploadedBytes: undefined, 74 | // By default, failed (abort or error) file uploads are removed from the 75 | // global progress calculation. Set the following option to false to 76 | // prevent recalculating the global progress data: 77 | recalculateProgress: true, 78 | 79 | // Additional form data to be sent along with the file uploads can be set 80 | // using this option, which accepts an array of objects with name and 81 | // value properties, a function returning such an array, a FormData 82 | // object (for XHR file uploads), or a simple object. 83 | // The form of the first fileInput is given as parameter to the function: 84 | formData: function (form) { 85 | return form.serializeArray(); 86 | }, 87 | 88 | // The add callback is invoked as soon as files are added to the fileupload 89 | // widget (via file input selection, drag & drop or add API call). 90 | // If the singleFileUploads option is enabled, this callback will be 91 | // called once for each file in the selection for XHR file uplaods, else 92 | // once for each file selection. 93 | // The upload starts when the submit method is invoked on the data parameter. 94 | // The data object contains a files property holding the added files 95 | // and allows to override plugin options as well as define ajax settings. 96 | // Listeners for this callback can also be bound the following way: 97 | // .bind('fileuploadadd', func); 98 | // data.submit() returns a Promise object and allows to attach additional 99 | // handlers using jQuery's Deferred callbacks: 100 | // data.submit().done(func).fail(func).always(func); 101 | add: function (e, data) { 102 | data.submit(); 103 | }, 104 | 105 | // Other callbacks: 106 | // Callback for the start of each file upload request: 107 | // send: function (e, data) {}, // .bind('fileuploadsend', func); 108 | // Callback for successful uploads: 109 | // done: function (e, data) {}, // .bind('fileuploaddone', func); 110 | // Callback for failed (abort or error) uploads: 111 | // fail: function (e, data) {}, // .bind('fileuploadfail', func); 112 | // Callback for completed (success, abort or error) requests: 113 | // always: function (e, data) {}, // .bind('fileuploadalways', func); 114 | // Callback for upload progress events: 115 | // progress: function (e, data) {}, // .bind('fileuploadprogress', func); 116 | // Callback for global upload progress events: 117 | // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); 118 | // Callback for uploads start, equivalent to the global ajaxStart event: 119 | // start: function (e) {}, // .bind('fileuploadstart', func); 120 | // Callback for uploads stop, equivalent to the global ajaxStop event: 121 | // stop: function (e) {}, // .bind('fileuploadstop', func); 122 | // Callback for change events of the fileInput collection: 123 | // change: function (e, data) {}, // .bind('fileuploadchange', func); 124 | // Callback for drop events of the dropZone collection: 125 | // drop: function (e, data) {}, // .bind('fileuploaddrop', func); 126 | // Callback for dragover events of the dropZone collection: 127 | // dragover: function (e) {}, // .bind('fileuploaddragover', func); 128 | 129 | // The plugin options are used as settings object for the ajax calls. 130 | // The following are jQuery ajax settings required for the file uploads: 131 | processData: false, 132 | contentType: false, 133 | cache: false 134 | }, 135 | 136 | // A list of options that require a refresh after assigning a new value: 137 | _refreshOptionsList: ['namespace', 'dropZone', 'fileInput'], 138 | 139 | _isXHRUpload: function (options) { 140 | var undef = 'undefined'; 141 | return !options.forceIframeTransport && 142 | typeof XMLHttpRequestUpload !== undef && typeof File !== undef && 143 | (!options.multipart || typeof FormData !== undef); 144 | }, 145 | 146 | _getFormData: function (options) { 147 | var formData; 148 | if (typeof options.formData === 'function') { 149 | return options.formData(options.form); 150 | } else if ($.isArray(options.formData)) { 151 | return options.formData; 152 | } else if (options.formData) { 153 | formData = []; 154 | $.each(options.formData, function (name, value) { 155 | formData.push({name: name, value: value}); 156 | }); 157 | return formData; 158 | } 159 | return []; 160 | }, 161 | 162 | _getTotal: function (files) { 163 | var total = 0; 164 | $.each(files, function (index, file) { 165 | total += file.size || 1; 166 | }); 167 | return total; 168 | }, 169 | 170 | _onProgress: function (e, data) { 171 | if (e.lengthComputable) { 172 | var total = data.total || this._getTotal(data.files), 173 | loaded = parseInt( 174 | e.loaded / e.total * (data.chunkSize || total), 175 | 10 176 | ) + (data.uploadedBytes || 0); 177 | this._loaded += loaded - (data.loaded || data.uploadedBytes || 0); 178 | data.lengthComputable = true; 179 | data.loaded = loaded; 180 | data.total = total; 181 | // Trigger a custom progress event with a total data property set 182 | // to the file size(s) of the current upload and a loaded data 183 | // property calculated accordingly: 184 | this._trigger('progress', e, data); 185 | // Trigger a global progress event for all current file uploads, 186 | // including ajax calls queued for sequential file uploads: 187 | this._trigger('progressall', e, { 188 | lengthComputable: true, 189 | loaded: this._loaded, 190 | total: this._total 191 | }); 192 | } 193 | }, 194 | 195 | _initProgressListener: function (options) { 196 | var that = this, 197 | xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); 198 | // Accesss to the native XHR object is required to add event listeners 199 | // for the upload progress event: 200 | if (xhr.upload && xhr.upload.addEventListener) { 201 | xhr.upload.addEventListener('progress', function (e) { 202 | that._onProgress(e, options); 203 | }, false); 204 | options.xhr = function () { 205 | return xhr; 206 | }; 207 | } 208 | }, 209 | 210 | _initXHRData: function (options) { 211 | var formData, 212 | file = options.files[0]; 213 | if (!options.multipart || options.blob) { 214 | // For non-multipart uploads and chunked uploads, 215 | // file meta data is not part of the request body, 216 | // so we transmit this data as part of the HTTP headers. 217 | // For cross domain requests, these headers must be allowed 218 | // via Access-Control-Allow-Headers or removed using 219 | // the beforeSend callback: 220 | options.headers = $.extend(options.headers, { 221 | 'X-File-Name': file.name, 222 | 'X-File-Type': file.type, 223 | 'X-File-Size': file.size 224 | }); 225 | if (!options.blob) { 226 | // Non-chunked non-multipart upload: 227 | options.contentType = file.type; 228 | options.data = file; 229 | } else if (!options.multipart) { 230 | // Chunked non-multipart upload: 231 | options.contentType = 'application/octet-stream'; 232 | options.data = options.blob; 233 | } 234 | } 235 | if (options.multipart && typeof FormData !== 'undefined') { 236 | if (options.formData instanceof FormData) { 237 | formData = options.formData; 238 | } else { 239 | formData = new FormData(); 240 | $.each(this._getFormData(options), function (index, field) { 241 | formData.append(field.name, field.value); 242 | }); 243 | } 244 | if (options.blob) { 245 | formData.append(options.paramName, options.blob); 246 | } else { 247 | $.each(options.files, function (index, file) { 248 | // File objects are also Blob instances. 249 | // This check allows the tests to run with 250 | // dummy objects: 251 | if (file instanceof Blob) { 252 | formData.append(options.paramName, file); 253 | } 254 | }); 255 | } 256 | options.data = formData; 257 | } 258 | // Blob reference is not needed anymore, free memory: 259 | options.blob = null; 260 | }, 261 | 262 | _initIframeSettings: function (options) { 263 | // Setting the dataType to iframe enables the iframe transport: 264 | options.dataType = 'iframe ' + (options.dataType || ''); 265 | // The iframe transport accepts a serialized array as form data: 266 | options.formData = this._getFormData(options); 267 | }, 268 | 269 | _initDataSettings: function (options) { 270 | if (this._isXHRUpload(options)) { 271 | if (!this._chunkedUpload(options, true)) { 272 | if (!options.data) { 273 | this._initXHRData(options); 274 | } 275 | this._initProgressListener(options); 276 | } 277 | } else { 278 | this._initIframeSettings(options); 279 | } 280 | }, 281 | 282 | _initFormSettings: function (options) { 283 | // Retrieve missing options from the input field and the 284 | // associated form, if available: 285 | if (!options.form || !options.form.length) { 286 | options.form = $(options.fileInput.prop('form')); 287 | } 288 | if (!options.paramName) { 289 | options.paramName = options.fileInput.prop('name') || 290 | 'files[]'; 291 | } 292 | if (!options.url) { 293 | options.url = options.form.prop('action') || location.href; 294 | } 295 | // The HTTP request method must be "POST" or "PUT": 296 | options.type = (options.type || options.form.prop('method') || '') 297 | .toUpperCase(); 298 | if (options.type !== 'POST' && options.type !== 'PUT') { 299 | options.type = 'POST'; 300 | } 301 | }, 302 | 303 | _getAJAXSettings: function (data) { 304 | var options = $.extend({}, this.options, data); 305 | this._initFormSettings(options); 306 | this._initDataSettings(options); 307 | return options; 308 | }, 309 | 310 | // Maps jqXHR callbacks to the equivalent 311 | // methods of the given Promise object: 312 | _enhancePromise: function (promise) { 313 | promise.success = promise.done; 314 | promise.error = promise.fail; 315 | promise.complete = promise.always; 316 | return promise; 317 | }, 318 | 319 | // Creates and returns a Promise object enhanced with 320 | // the jqXHR methods abort, success, error and complete: 321 | _getXHRPromise: function (resolveOrReject, context, args) { 322 | var dfd = $.Deferred(), 323 | promise = dfd.promise(); 324 | context = context || this.options.context || promise; 325 | if (resolveOrReject === true) { 326 | dfd.resolveWith(context, args); 327 | } else if (resolveOrReject === false) { 328 | dfd.rejectWith(context, args); 329 | } 330 | promise.abort = dfd.promise; 331 | return this._enhancePromise(promise); 332 | }, 333 | 334 | // Uploads a file in multiple, sequential requests 335 | // by splitting the file up in multiple blob chunks. 336 | // If the second parameter is true, only tests if the file 337 | // should be uploaded in chunks, but does not invoke any 338 | // upload requests: 339 | _chunkedUpload: function (options, testOnly) { 340 | var that = this, 341 | file = options.files[0], 342 | fs = file.size, 343 | ub = options.uploadedBytes = options.uploadedBytes || 0, 344 | mcs = options.maxChunkSize || fs, 345 | // Use the Blob methods with the slice implementation 346 | // according to the W3C Blob API specification: 347 | slice = file.webkitSlice || file.mozSlice || file.slice, 348 | upload, 349 | n, 350 | jqXHR, 351 | pipe; 352 | if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || 353 | options.data) { 354 | return false; 355 | } 356 | if (testOnly) { 357 | return true; 358 | } 359 | if (ub >= fs) { 360 | file.error = 'uploadedBytes'; 361 | return this._getXHRPromise(false); 362 | } 363 | // n is the number of blobs to upload, 364 | // calculated via filesize, uploaded bytes and max chunk size: 365 | n = Math.ceil((fs - ub) / mcs); 366 | // The chunk upload method accepting the chunk number as parameter: 367 | upload = function (i) { 368 | if (!i) { 369 | return that._getXHRPromise(true); 370 | } 371 | // Upload the blobs in sequential order: 372 | return upload(i -= 1).pipe(function () { 373 | // Clone the options object for each chunk upload: 374 | var o = $.extend({}, options); 375 | o.blob = slice.call( 376 | file, 377 | ub + i * mcs, 378 | ub + (i + 1) * mcs 379 | ); 380 | // Store the current chunk size, as the blob itself 381 | // will be dereferenced after data processing: 382 | o.chunkSize = o.blob.size; 383 | // Process the upload data (the blob and potential form data): 384 | that._initXHRData(o); 385 | // Add progress listeners for this chunk upload: 386 | that._initProgressListener(o); 387 | jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context)) 388 | .done(function () { 389 | // Create a progress event if upload is done and 390 | // no progress event has been invoked for this chunk: 391 | if (!o.loaded) { 392 | that._onProgress($.Event('progress', { 393 | lengthComputable: true, 394 | loaded: o.chunkSize, 395 | total: o.chunkSize 396 | }), o); 397 | } 398 | options.uploadedBytes = o.uploadedBytes 399 | += o.chunkSize; 400 | }); 401 | return jqXHR; 402 | }); 403 | }; 404 | // Return the piped Promise object, enhanced with an abort method, 405 | // which is delegated to the jqXHR object of the current upload, 406 | // and jqXHR callbacks mapped to the equivalent Promise methods: 407 | pipe = upload(n); 408 | pipe.abort = function () { 409 | return jqXHR.abort(); 410 | }; 411 | return this._enhancePromise(pipe); 412 | }, 413 | 414 | _beforeSend: function (e, data) { 415 | if (this._active === 0) { 416 | // the start callback is triggered when an upload starts 417 | // and no other uploads are currently running, 418 | // equivalent to the global ajaxStart event: 419 | this._trigger('start'); 420 | } 421 | this._active += 1; 422 | // Initialize the global progress values: 423 | this._loaded += data.uploadedBytes || 0; 424 | this._total += this._getTotal(data.files); 425 | }, 426 | 427 | _onDone: function (result, textStatus, jqXHR, options) { 428 | if (!this._isXHRUpload(options)) { 429 | // Create a progress event for each iframe load: 430 | this._onProgress($.Event('progress', { 431 | lengthComputable: true, 432 | loaded: 1, 433 | total: 1 434 | }), options); 435 | } 436 | options.result = result; 437 | options.textStatus = textStatus; 438 | options.jqXHR = jqXHR; 439 | this._trigger('done', null, options); 440 | }, 441 | 442 | _onFail: function (jqXHR, textStatus, errorThrown, options) { 443 | options.jqXHR = jqXHR; 444 | options.textStatus = textStatus; 445 | options.errorThrown = errorThrown; 446 | this._trigger('fail', null, options); 447 | if (options.recalculateProgress) { 448 | // Remove the failed (error or abort) file upload from 449 | // the global progress calculation: 450 | this._loaded -= options.loaded || options.uploadedBytes || 0; 451 | this._total -= options.total || this._getTotal(options.files); 452 | } 453 | }, 454 | 455 | _onAlways: function (result, textStatus, jqXHR, errorThrown, options) { 456 | this._active -= 1; 457 | options.result = result; 458 | options.textStatus = textStatus; 459 | options.jqXHR = jqXHR; 460 | options.errorThrown = errorThrown; 461 | this._trigger('always', null, options); 462 | if (this._active === 0) { 463 | // The stop callback is triggered when all uploads have 464 | // been completed, equivalent to the global ajaxStop event: 465 | this._trigger('stop'); 466 | // Reset the global progress values: 467 | this._loaded = this._total = 0; 468 | } 469 | }, 470 | 471 | _onSend: function (e, data) { 472 | var that = this, 473 | jqXHR, 474 | pipe, 475 | options = that._getAJAXSettings(data), 476 | send = function (resolve, args) { 477 | jqXHR = jqXHR || ( 478 | (resolve !== false && 479 | that._trigger('send', e, options) !== false && 480 | (that._chunkedUpload(options) || $.ajax(options))) || 481 | that._getXHRPromise(false, options.context, args) 482 | ).done(function (result, textStatus, jqXHR) { 483 | that._onDone(result, textStatus, jqXHR, options); 484 | }).fail(function (jqXHR, textStatus, errorThrown) { 485 | that._onFail(jqXHR, textStatus, errorThrown, options); 486 | }).always(function (a1, a2, a3) { 487 | if (a3 && a3.done) { 488 | that._onAlways(a1, a2, a3, undefined, options); 489 | } else { 490 | that._onAlways(undefined, a2, a1, a3, options); 491 | } 492 | }); 493 | return jqXHR; 494 | }; 495 | this._beforeSend(e, options); 496 | if (this.options.sequentialUploads) { 497 | // Return the piped Promise object, enhanced with an abort method, 498 | // which is delegated to the jqXHR object of the current upload, 499 | // and jqXHR callbacks mapped to the equivalent Promise methods: 500 | pipe = (this._sequence = this._sequence.pipe(send, send)); 501 | pipe.abort = function () { 502 | if (!jqXHR) { 503 | return send(false, [undefined, 'abort', 'abort']); 504 | } 505 | return jqXHR.abort(); 506 | }; 507 | return this._enhancePromise(pipe); 508 | } 509 | return send(); 510 | }, 511 | 512 | _onAdd: function (e, data) { 513 | var that = this, 514 | result = true, 515 | options = $.extend({}, this.options, data); 516 | if (options.singleFileUploads && this._isXHRUpload(options)) { 517 | $.each(data.files, function (index, file) { 518 | var newData = $.extend({}, data, {files: [file]}); 519 | newData.submit = function () { 520 | return that._onSend(e, newData); 521 | }; 522 | return (result = that._trigger('add', e, newData)); 523 | }); 524 | return result; 525 | } else if (data.files.length) { 526 | data = $.extend({}, data); 527 | data.submit = function () { 528 | return that._onSend(e, data); 529 | }; 530 | return this._trigger('add', e, data); 531 | } 532 | }, 533 | 534 | // File Normalization for Gecko 1.9.1 (Firefox 3.5) support: 535 | _normalizeFile: function (index, file) { 536 | if (file.name === undefined && file.size === undefined) { 537 | file.name = file.fileName; 538 | file.size = file.fileSize; 539 | } 540 | }, 541 | 542 | _replaceFileInput: function (input) { 543 | var inputClone = input.clone(true); 544 | $('
').append(inputClone)[0].reset(); 545 | // Detaching allows to insert the fileInput on another form 546 | // without loosing the file input value: 547 | input.after(inputClone).detach(); 548 | // Replace the original file input element in the fileInput 549 | // collection with the clone, which has been copied including 550 | // event handlers: 551 | this.options.fileInput = this.options.fileInput.map(function (i, el) { 552 | if (el === input[0]) { 553 | return inputClone[0]; 554 | } 555 | return el; 556 | }); 557 | }, 558 | 559 | _onChange: function (e) { 560 | var that = e.data.fileupload, 561 | data = { 562 | files: $.each($.makeArray(e.target.files), that._normalizeFile), 563 | fileInput: $(e.target), 564 | form: $(e.target.form) 565 | }; 566 | if (!data.files.length) { 567 | // If the files property is not available, the browser does not 568 | // support the File API and we add a pseudo File object with 569 | // the input value as name with path information removed: 570 | data.files = [{name: e.target.value.replace(/^.*\\/, '')}]; 571 | } 572 | // Store the form reference as jQuery data for other event handlers, 573 | // as the form property is not available after replacing the file input: 574 | if (data.form.length) { 575 | data.fileInput.data('blueimp.fileupload.form', data.form); 576 | } else { 577 | data.form = data.fileInput.data('blueimp.fileupload.form'); 578 | } 579 | if (that.options.replaceFileInput) { 580 | that._replaceFileInput(data.fileInput); 581 | } 582 | if (that._trigger('change', e, data) === false || 583 | that._onAdd(e, data) === false) { 584 | return false; 585 | } 586 | }, 587 | 588 | _onDrop: function (e) { 589 | var that = e.data.fileupload, 590 | dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer, 591 | data = { 592 | files: $.each( 593 | $.makeArray(dataTransfer && dataTransfer.files), 594 | that._normalizeFile 595 | ) 596 | }; 597 | if (that._trigger('drop', e, data) === false || 598 | that._onAdd(e, data) === false) { 599 | return false; 600 | } 601 | e.preventDefault(); 602 | }, 603 | 604 | _onDragOver: function (e) { 605 | var that = e.data.fileupload, 606 | dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer; 607 | if (that._trigger('dragover', e) === false) { 608 | return false; 609 | } 610 | if (dataTransfer) { 611 | dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy'; 612 | } 613 | e.preventDefault(); 614 | }, 615 | 616 | _initEventHandlers: function () { 617 | var ns = this.options.namespace || this.name; 618 | this.options.dropZone 619 | .bind('dragover.' + ns, {fileupload: this}, this._onDragOver) 620 | .bind('drop.' + ns, {fileupload: this}, this._onDrop); 621 | this.options.fileInput 622 | .bind('change.' + ns, {fileupload: this}, this._onChange); 623 | }, 624 | 625 | _destroyEventHandlers: function () { 626 | var ns = this.options.namespace || this.name; 627 | this.options.dropZone 628 | .unbind('dragover.' + ns, this._onDragOver) 629 | .unbind('drop.' + ns, this._onDrop); 630 | this.options.fileInput 631 | .unbind('change.' + ns, this._onChange); 632 | }, 633 | 634 | _beforeSetOption: function (key, value) { 635 | this._destroyEventHandlers(); 636 | }, 637 | 638 | _afterSetOption: function (key, value) { 639 | var options = this.options; 640 | if (!options.fileInput) { 641 | options.fileInput = $(); 642 | } 643 | if (!options.dropZone) { 644 | options.dropZone = $(); 645 | } 646 | this._initEventHandlers(); 647 | }, 648 | 649 | _setOption: function (key, value) { 650 | var refresh = $.inArray(key, this._refreshOptionsList) !== -1; 651 | if (refresh) { 652 | this._beforeSetOption(key, value); 653 | } 654 | $.Widget.prototype._setOption.call(this, key, value); 655 | if (refresh) { 656 | this._afterSetOption(key, value); 657 | } 658 | }, 659 | 660 | _create: function () { 661 | var options = this.options; 662 | if (options.fileInput === undefined) { 663 | options.fileInput = this.element.is('input:file') ? 664 | this.element : this.element.find('input:file'); 665 | } else if (!options.fileInput) { 666 | options.fileInput = $(); 667 | } 668 | if (!options.dropZone) { 669 | options.dropZone = $(); 670 | } 671 | this._sequence = this._getXHRPromise(true); 672 | this._active = this._loaded = this._total = 0; 673 | this._initEventHandlers(); 674 | }, 675 | 676 | destroy: function () { 677 | this._destroyEventHandlers(); 678 | $.Widget.prototype.destroy.call(this); 679 | }, 680 | 681 | enable: function () { 682 | $.Widget.prototype.enable.call(this); 683 | this._initEventHandlers(); 684 | }, 685 | 686 | disable: function () { 687 | this._destroyEventHandlers(); 688 | $.Widget.prototype.disable.call(this); 689 | }, 690 | 691 | // This method is exposed to the widget API and allows adding files 692 | // using the fileupload API. The data parameter accepts an object which 693 | // must have a files property and can contain additional options: 694 | // .fileupload('add', {files: filesList}); 695 | add: function (data) { 696 | if (!data || this.options.disabled) { 697 | return; 698 | } 699 | data.files = $.each($.makeArray(data.files), this._normalizeFile); 700 | this._onAdd(null, data); 701 | }, 702 | 703 | // This method is exposed to the widget API and allows sending files 704 | // using the fileupload API. The data parameter accepts an object which 705 | // must have a files property and can contain additional options: 706 | // .fileupload('send', {files: filesList}); 707 | // The method returns a Promise object for the file upload call. 708 | send: function (data) { 709 | if (data && !this.options.disabled) { 710 | data.files = $.each($.makeArray(data.files), this._normalizeFile); 711 | if (data.files.length) { 712 | return this._onSend(null, data); 713 | } 714 | } 715 | return this._getXHRPromise(false, data && data.context); 716 | } 717 | 718 | }); 719 | 720 | }(jQuery)); -------------------------------------------------------------------------------- /static/js/jquery.iframe-transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Iframe Transport Plugin 1.2.2 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://creativecommons.org/licenses/MIT/ 10 | */ 11 | 12 | /*jslint unparam: true */ 13 | /*global jQuery */ 14 | 15 | (function ($) { 16 | 'use strict'; 17 | 18 | // Helper variable to create unique names for the transport iframes: 19 | var counter = 0; 20 | 21 | // The iframe transport accepts three additional options: 22 | // options.fileInput: a jQuery collection of file input fields 23 | // options.paramName: the parameter name for the file form data, 24 | // overrides the name property of the file input field(s) 25 | // options.formData: an array of objects with name and value properties, 26 | // equivalent to the return data of .serializeArray(), e.g.: 27 | // [{name: a, value: 1}, {name: b, value: 2}] 28 | $.ajaxTransport('iframe', function (options, originalOptions, jqXHR) { 29 | if (options.type === 'POST' || options.type === 'GET') { 30 | var form, 31 | iframe; 32 | return { 33 | send: function (headers, completeCallback) { 34 | form = $('
'); 35 | // javascript:false as initial iframe src 36 | // prevents warning popups on HTTPS in IE6. 37 | // IE versions below IE8 cannot set the name property of 38 | // elements that have already been added to the DOM, 39 | // so we set the name along with the iframe HTML markup: 40 | iframe = $( 41 | '' 43 | ).bind('load', function () { 44 | var fileInputClones; 45 | iframe 46 | .unbind('load') 47 | .bind('load', function () { 48 | var response; 49 | // Wrap in a try/catch block to catch exceptions thrown 50 | // when trying to access cross-domain iframe contents: 51 | try { 52 | response = iframe.contents(); 53 | // Google Chrome and Firefox do not throw an 54 | // exception when calling iframe.contents() on 55 | // cross-domain requests, so we unify the response: 56 | if (!response.length || !response[0].firstChild) { 57 | throw new Error(); 58 | } 59 | } catch (e) { 60 | response = undefined; 61 | } 62 | // The complete callback returns the 63 | // iframe content document as response object: 64 | completeCallback( 65 | 200, 66 | 'success', 67 | {'iframe': response} 68 | ); 69 | // Fix for IE endless progress bar activity bug 70 | // (happens on form submits to iframe targets): 71 | $('') 72 | .appendTo(form); 73 | form.remove(); 74 | }); 75 | form 76 | .prop('target', iframe.prop('name')) 77 | .prop('action', options.url) 78 | .prop('method', options.type); 79 | if (options.formData) { 80 | $.each(options.formData, function (index, field) { 81 | $('') 82 | .prop('name', field.name) 83 | .val(field.value) 84 | .appendTo(form); 85 | }); 86 | } 87 | if (options.fileInput && options.fileInput.length && 88 | options.type === 'POST') { 89 | fileInputClones = options.fileInput.clone(); 90 | // Insert a clone for each file input field: 91 | options.fileInput.after(function (index) { 92 | return fileInputClones[index]; 93 | }); 94 | if (options.paramName) { 95 | options.fileInput.each(function () { 96 | $(this).prop('name', options.paramName); 97 | }); 98 | } 99 | // Appending the file input fields to the hidden form 100 | // removes them from their original location: 101 | form 102 | .append(options.fileInput) 103 | .prop('enctype', 'multipart/form-data') 104 | // enctype must be set as encoding for IE: 105 | .prop('encoding', 'multipart/form-data'); 106 | } 107 | form.submit(); 108 | // Insert the file input fields at their original location 109 | // by replacing the clones with the originals: 110 | if (fileInputClones && fileInputClones.length) { 111 | options.fileInput.each(function (index, input) { 112 | var clone = $(fileInputClones[index]); 113 | $(input).prop('name', clone.prop('name')); 114 | clone.replaceWith(input); 115 | }); 116 | } 117 | }); 118 | form.append(iframe).appendTo('body'); 119 | }, 120 | abort: function () { 121 | if (iframe) { 122 | // javascript:false as iframe src aborts the request 123 | // and prevents warning popups on HTTPS in IE6. 124 | // concat is used to avoid the "Script URL" JSLint error: 125 | iframe 126 | .unbind('load') 127 | .prop('src', 'javascript'.concat(':false;')); 128 | } 129 | if (form) { 130 | form.remove(); 131 | } 132 | } 133 | }; 134 | } 135 | }); 136 | 137 | // The iframe transport returns the iframe content document as response. 138 | // The following adds converters from iframe to text, json, html, and script: 139 | $.ajaxSetup({ 140 | converters: { 141 | 'iframe text': function (iframe) { 142 | return iframe.text(); 143 | }, 144 | 'iframe json': function (iframe) { 145 | return $.parseJSON(iframe.text()); 146 | }, 147 | 'iframe html': function (iframe) { 148 | return iframe.find('body').html(); 149 | }, 150 | 'iframe script': function (iframe) { 151 | return $.globalEval(iframe.text()); 152 | } 153 | } 154 | }); 155 | 156 | }(jQuery)); -------------------------------------------------------------------------------- /static/js/jquery_fix_csrf.js: -------------------------------------------------------------------------------- 1 | // jQuery alteration which send CSRF token on each ajax request in order to pass Django CSRF 2 | $(document).ajaxSend(function(event, xhr, settings) { 3 | function getCookie(name) { 4 | var cookieValue = null; 5 | if (document.cookie && document.cookie != '') { 6 | var cookies = document.cookie.split(';'); 7 | for (var i = 0; i < cookies.length; i++) { 8 | var cookie = jQuery.trim(cookies[i]); 9 | // Does this cookie string begin with the name we want? 10 | if (cookie.substring(0, name.length + 1) == (name + '=')) { 11 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 12 | break; 13 | } 14 | } 15 | } 16 | return cookieValue; 17 | } 18 | 19 | function sameOrigin(url) { 20 | // url could be relative or scheme relative or absolute 21 | var host = document.location.host; // host + port 22 | var protocol = document.location.protocol; 23 | var sr_origin = '//' + host; 24 | var origin = protocol + sr_origin; 25 | // Allow absolute or scheme relative URLs to same origin 26 | return (url == origin || url.slice(0, origin.length + 1) == origin + '/') || 27 | (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') || 28 | // or any other URL that isn't scheme relative or absolute i.e relative. 29 | !(/^(\/\/|http:|https:).*/.test(url)); 30 | } 31 | 32 | function safeMethod(method) { 33 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); 34 | } 35 | 36 | if (!safeMethod(settings.type) && sameOrigin(settings.url)) { 37 | xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); 38 | } 39 | }); -------------------------------------------------------------------------------- /templates/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | 21 | Demo Implementation of jQuery Uploader 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |
91 |
92 | {% csrf_token %} 93 |
94 | 98 | 99 | 100 | 101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | 109 | 136 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /upload/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miki725/Django-jQuery-File-Uploader-Integration-demo/98ebf63caebeeb9c78c0207eb553dd3c8320087f/upload/__init__.py -------------------------------------------------------------------------------- /upload/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /upload/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /upload/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns('upload.views', 4 | 5 | # ================================== # 6 | # Upload URLS # 7 | # ================================== # 8 | 9 | # Main template with the gui to upload 10 | # If you want you can expand this so each action of the uploader 11 | # will have it's own url. Now a single view is responsible for 12 | # distinguishing what it is suppose to do. 13 | (r'^', 'Upload'), 14 | 15 | ) 16 | -------------------------------------------------------------------------------- /upload/views.py: -------------------------------------------------------------------------------- 1 | # imports 2 | 3 | # import the django settings 4 | from django.conf import settings 5 | # for generating json 6 | from django.utils import simplejson 7 | # for loading template 8 | from django.template import Context, loader 9 | # for csrf 10 | from django.core.context_processors import csrf 11 | # for HTTP response 12 | from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect 13 | # for os manipulations 14 | import os 15 | 16 | 17 | def Upload(request): 18 | """ 19 | 20 | ## View for file uploads ## 21 | 22 | It does the following actions: 23 | - displays a template if no action have been specified 24 | - upload a file into unique temporary directory 25 | unique directory for an upload session 26 | meaning when user opens up an upload page, all upload actions 27 | while being on that page will be uploaded to unique directory. 28 | as soon as user will reload, files will be uploaded to a different 29 | unique directory 30 | - delete an uploaded file 31 | 32 | ## How Single View Multi-functions ## 33 | 34 | If the user just goes to a the upload url (e.g. '/upload/'), the request.method will be "GET" 35 | Or you can think of it as request.method will NOT be "POST" 36 | Therefore the view will always return the upload template 37 | 38 | If on the other side the method is POST, that means some sort of upload action 39 | has to be done. That could be either uploading a file or deleting a file 40 | 41 | For deleting files, there is the same url (e.g. '/upload/'), except it has an 42 | extra query parameter. Meaning the url will have '?' in it. 43 | In this implementation the query will simply be '?f=filename_of_the_file_to_be_removed' 44 | 45 | If the request has no query parameters, file is being uploaded. 46 | 47 | """ 48 | 49 | # used to generate random unique id 50 | import uuid 51 | 52 | # settings for the file upload 53 | # you can define other parameters here 54 | # and check validity late in the code 55 | options = { 56 | # the maximum file size (must be in bytes) 57 | "maxfilesize": 2 * 2 ** 20, # 2 Mb 58 | # the minimum file size (must be in bytes) 59 | "minfilesize": 1 * 2 ** 10, # 1 Kb 60 | # the file types which are going to be allowed for upload 61 | # must be a mimetype 62 | "acceptedformats": ( 63 | "image/jpeg", 64 | "image/png", 65 | ) 66 | } 67 | 68 | 69 | # POST request 70 | # meaning user has triggered an upload action 71 | if request.method == 'POST': 72 | # figure out the path where files will be uploaded to 73 | # PROJECT_ROOT is from the settings file 74 | temp_path = os.path.join(settings.PROJECT_ROOT, "tmp") 75 | 76 | # if 'f' query parameter is not specified 77 | # file is being uploaded 78 | if not ("f" in request.GET.keys()): # upload file 79 | 80 | # make sure some files have been uploaded 81 | if not request.FILES: 82 | return HttpResponseBadRequest('Must upload a file') 83 | 84 | # make sure unique id is specified - VERY IMPORTANT 85 | # this is necessary because of the following: 86 | # we want users to upload to a unique directory 87 | # however the uploader will make independent requests to the server 88 | # to upload each file, so there has to be a method for all these files 89 | # to be recognized as a single batch of files 90 | # a unique id for each session will do the job 91 | if not request.POST[u"uid"]: 92 | return HttpResponseBadRequest("UID not specified.") 93 | # if here, uid has been specified, so record it 94 | uid = request.POST[u"uid"] 95 | 96 | # update the temporary path by creating a sub-folder within 97 | # the upload folder with the uid name 98 | temp_path = os.path.join(temp_path, uid) 99 | 100 | # get the uploaded file 101 | file = request.FILES[u'files[]'] 102 | 103 | # initialize the error 104 | # If error occurs, this will have the string error message so 105 | # uploader can display the appropriate message 106 | error = False 107 | 108 | # check against options for errors 109 | 110 | # file size 111 | if file.size > options["maxfilesize"]: 112 | error = "maxFileSize" 113 | if file.size < options["minfilesize"]: 114 | error = "minFileSize" 115 | # allowed file type 116 | if file.content_type not in options["acceptedformats"]: 117 | error = "acceptFileTypes" 118 | 119 | 120 | # the response data which will be returned to the uploader as json 121 | response_data = { 122 | "name": file.name, 123 | "size": file.size, 124 | "type": file.content_type 125 | } 126 | 127 | # if there was an error, add error message to response_data and return 128 | if error: 129 | # append error message 130 | response_data["error"] = error 131 | # generate json 132 | response_data = simplejson.dumps([response_data]) 133 | # return response to uploader with error 134 | # so it can display error message 135 | return HttpResponse(response_data, mimetype='application/json') 136 | 137 | 138 | # make temporary dir if not exists already 139 | if not os.path.exists(temp_path): 140 | os.makedirs(temp_path) 141 | 142 | # get the absolute path of where the uploaded file will be saved 143 | # all add some random data to the filename in order to avoid conflicts 144 | # when user tries to upload two files with same filename 145 | filename = os.path.join(temp_path, str(uuid.uuid4()) + file.name) 146 | # open the file handler with write binary mode 147 | destination = open(filename, "wb+") 148 | # save file data into the disk 149 | # use the chunk method in case the file is too big 150 | # in order not to clutter the system memory 151 | for chunk in file.chunks(): 152 | destination.write(chunk) 153 | # close the file 154 | destination.close() 155 | 156 | # here you can add the file to a database, 157 | # move it around, 158 | # do anything, 159 | # or do nothing and enjoy the demo 160 | # just make sure if you do move the file around, 161 | # then make sure to update the delete_url which will be send to the server 162 | # or not include that information at all in the response... 163 | 164 | # allows to generate properly formatted and escaped url queries 165 | import urllib 166 | 167 | # url for deleting the file in case user decides to delete it 168 | response_data["delete_url"] = request.path + "?" + urllib.urlencode( 169 | {"f": uid + "/" + os.path.split(filename)[1]}) 170 | # specify the delete type - must be POST for csrf 171 | response_data["delete_type"] = "POST" 172 | 173 | # generate the json data 174 | response_data = simplejson.dumps([response_data]) 175 | # response type 176 | response_type = "application/json" 177 | 178 | # QUIRK HERE 179 | # in jQuey uploader, when it falls back to uploading using iFrames 180 | # the response content type has to be text/html 181 | # if json will be send, error will occur 182 | # if iframe is sending the request, it's headers are a little different compared 183 | # to the jQuery ajax request 184 | # they have different set of HTTP_ACCEPT values 185 | # so if the text/html is present, file was uploaded using jFrame because 186 | # that value is not in the set when uploaded by XHR 187 | if "text/html" in request.META["HTTP_ACCEPT"]: 188 | response_type = "text/html" 189 | 190 | # return the data to the uploading plugin 191 | return HttpResponse(response_data, mimetype=response_type) 192 | 193 | else: # file has to be deleted 194 | 195 | # get the file path by getting it from the query (e.g. '?f=filename.here') 196 | filepath = os.path.join(temp_path, request.GET["f"]) 197 | 198 | # make sure file exists 199 | # if not return error 200 | if not os.path.isfile(filepath): 201 | return HttpResponseBadRequest("File does not exist") 202 | 203 | # delete the file 204 | # this step might not be a secure method so extra 205 | # security precautions might have to be taken 206 | os.remove(filepath) 207 | 208 | # generate true json result 209 | # in this case is it a json True value 210 | # if true is not returned, the file will not be removed from the upload queue 211 | response_data = simplejson.dumps(True) 212 | 213 | # return the result data 214 | # here it always has to be json 215 | return HttpResponse(response_data, mimetype="application/json") 216 | 217 | else: #GET 218 | # load the template 219 | t = loader.get_template("upload.html") 220 | c = Context({ 221 | # the unique id which will be used to get the folder path 222 | "uid": uuid.uuid4(), 223 | # these two are necessary to generate the jQuery templates 224 | # they have to be included here since they conflict with django template system 225 | "open_tv": u'{{', 226 | "close_tv": u'}}', 227 | # some of the parameters to be checked by javascript 228 | "maxfilesize": options["maxfilesize"], 229 | "minfilesize": options["minfilesize"], 230 | }) 231 | # add csrf token value to the dictionary 232 | c.update(csrf(request)) 233 | # return 234 | return HttpResponse(t.render(c)) 235 | 236 | -------------------------------------------------------------------------------- /urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include, url 2 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 3 | 4 | # Uncomment the next two lines to enable the admin: 5 | # from django.contrib import admin 6 | # admin.autodiscover() 7 | 8 | urlpatterns = patterns('', 9 | # Examples: 10 | # url(r'^$', 'djangoUpload.views.home', name='home'), 11 | # url(r'^djangoUpload/', include('djangoUpload.foo.urls')), 12 | 13 | # Uncomment the admin/doc line below to enable admin documentation: 14 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 15 | 16 | # Uncomment the next line to enable the admin: 17 | # url(r'^admin/', include(admin.site.urls)), 18 | 19 | # For the Django Jquery Upload 20 | (r'^upload/', include('upload.urls')), 21 | #(r'^upload/', 'upload.views.Upload')), 22 | 23 | #(r'^studyview/', include('studyview.urls')), 24 | 25 | ) 26 | 27 | urlpatterns += staticfiles_urlpatterns() 28 | --------------------------------------------------------------------------------