diff --git a/README.md b/README.md index 080ae1a..d9ff419 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,13 @@ $ export DJANGO_SETTINGS_MODULE=ojusomap.settings ## Install the Python Dependencies ```bash -$ pip3 install -r requirements.txt +$ pip3 install -r requirements-devel.txt ``` If you run into issues with `psycopg2` you may need to run the following: ```bash -$ pip uninstall psycopg2 && pip install --no-binary :all: psycopg2 +$ pip3 uninstall psycopg2 && pip3 install --no-binary :all: psycopg2 ``` ## Run The Migrations diff --git a/apps/files/__init__.py b/apps/files/__init__.py new file mode 100644 index 0000000..db621fa --- /dev/null +++ b/apps/files/__init__.py @@ -0,0 +1,3 @@ +""" AJAX file uploads + +based on https://simpleisbetterthancomplex.com/tutorial/2016/11/22/django-multiple-file-upload-using-ajax.html """ diff --git a/apps/files/admin.py b/apps/files/admin.py new file mode 100644 index 0000000..d90b977 --- /dev/null +++ b/apps/files/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from .models import File + +admin.site.register(File) diff --git a/apps/files/apps.py b/apps/files/apps.py new file mode 100644 index 0000000..c86f272 --- /dev/null +++ b/apps/files/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class FilesConfig(AppConfig): + name = 'files' diff --git a/apps/files/forms.py b/apps/files/forms.py new file mode 100644 index 0000000..9f0ada2 --- /dev/null +++ b/apps/files/forms.py @@ -0,0 +1,9 @@ +from django import forms + +from .models import File + + +class FileForm(forms.ModelForm): + class Meta: + model = File + exclude = ['user',] diff --git a/apps/files/migrations/0001_initial.py b/apps/files/migrations/0001_initial.py new file mode 100644 index 0000000..8e39e2a --- /dev/null +++ b/apps/files/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-04-23 02:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='File', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to='.')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/files/migrations/0002_file_user.py b/apps/files/migrations/0002_file_user.py new file mode 100644 index 0000000..12cf0f9 --- /dev/null +++ b/apps/files/migrations/0002_file_user.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-04-29 22:07 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('files', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='file', + name='user', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='files', to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] diff --git a/apps/files/migrations/__init__.py b/apps/files/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/files/models.py b/apps/files/models.py new file mode 100644 index 0000000..57a123d --- /dev/null +++ b/apps/files/models.py @@ -0,0 +1,23 @@ +from django.contrib.auth.models import User +from django.db import models + +from apps.map.models import CaseStudy, CaseStudyDraft + + +class BaseFile(models.Model): + file = models.FileField( + upload_to='.', + ) + user = models.ForeignKey( + User, related_name='files' + ) + + class Meta: + abstract = True + + def __str__(self): + return self.file.name + + +class File(BaseFile): + pass diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/.gitignore b/apps/files/static/files/jQuery-File-Upload-9.21.0/.gitignore new file mode 100644 index 0000000..29a41a8 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +*.pyc +node_modules diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/.jshintrc b/apps/files/static/files/jQuery-File-Upload-9.21.0/.jshintrc new file mode 100644 index 0000000..4ad82e6 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/.jshintrc @@ -0,0 +1,81 @@ +{ + "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) + "camelcase" : true, // true: Identifiers must be in camelCase + "curly" : true, // true: Require {} for every new block or scope + "eqeqeq" : true, // true: Require triple equals (===) for comparison + "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() + "immed" : true, // true: Require immediate invocations to be wrapped in parens + // e.g. `(function () { } ());` + "indent" : 4, // {int} Number of spaces to use for indentation + "latedef" : true, // true: Require variables/functions to be defined before being used + "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` + "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` + "noempty" : true, // true: Prohibit use of empty blocks + "nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment) + "plusplus" : false, // true: Prohibit use of `++` & `--` + "quotmark" : "single", // Quotation mark consistency: + // false : do nothing (default) + // true : ensure whatever is used is consistent + // "single" : require single quotes + // "double" : require double quotes + "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) + "unused" : true, // true: Require all defined variables be used + "strict" : true, // true: Requires all functions run in ES5 Strict Mode + "trailing" : true, // true: Prohibit trailing whitespaces + "maxparams" : false, // {int} Max number of formal params allowed per function + "maxdepth" : false, // {int} Max depth of nested blocks (within functions) + "maxstatements" : false, // {int} Max number statements per function + "maxcomplexity" : false, // {int} Max cyclomatic complexity per function + "maxlen" : false, // {int} Max number of characters per line + + // Relaxing + "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) + "boss" : false, // true: Tolerate assignments where comparisons would be expected + "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. + "eqnull" : false, // true: Tolerate use of `== null` + "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) + "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) + "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) + // (ex: `for each`, multiple try/catch, function expression…) + "evil" : false, // true: Tolerate use of `eval` and `new Function()` + "expr" : false, // true: Tolerate `ExpressionStatement` as Programs + "funcscope" : false, // true: Tolerate defining variables inside control statements" + "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') + "iterator" : false, // true: Tolerate using the `__iterator__` property + "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block + "laxbreak" : false, // true: Tolerate possibly unsafe line breakings + "laxcomma" : false, // true: Tolerate comma-first style coding + "loopfunc" : false, // true: Tolerate functions being defined in loops + "multistr" : false, // true: Tolerate multi-line strings + "proto" : false, // true: Tolerate using the `__proto__` property + "scripturl" : false, // true: Tolerate script-targeted URLs + "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment + "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` + "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation + "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` + "validthis" : false, // true: Tolerate using this in a non-constructor function + + // Environments + "browser" : false, // Web Browser (window, document, etc) + "couch" : false, // CouchDB + "devel" : false, // Development/debugging (alert, confirm, etc) + "dojo" : false, // Dojo Toolkit + "jquery" : false, // jQuery + "mootools" : false, // MooTools + "node" : false, // Node.js + "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) + "prototypejs" : false, // Prototype and Scriptaculous + "rhino" : false, // Rhino + "worker" : false, // Web Workers + "wsh" : false, // Windows Scripting Host + "yui" : false, // Yahoo User Interface + + // Legacy + "nomen" : true, // true: Prohibit dangling `_` in variables + "onevar" : true, // true: Allow only one `var` statement per function + "passfail" : false, // true: Stop on first error + "white" : true, // true: Check against strict whitespace and indentation rules + + // Custom Globals + "globals" : {} // additional predefined global variables +} diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/.npmignore b/apps/files/static/files/jQuery-File-Upload-9.21.0/.npmignore new file mode 100644 index 0000000..0530f5d --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/.npmignore @@ -0,0 +1,20 @@ +* +!css/jquery.fileupload-noscript.css +!css/jquery.fileupload-ui-noscript.css +!css/jquery.fileupload-ui.css +!css/jquery.fileupload.css +!img/loading.gif +!img/progressbar.gif +!js/cors/jquery.postmessage-transport.js +!js/cors/jquery.xdr-transport.js +!js/vendor/jquery.ui.widget.js +!js/jquery.fileupload-angular.js +!js/jquery.fileupload-audio.js +!js/jquery.fileupload-image.js +!js/jquery.fileupload-jquery-ui.js +!js/jquery.fileupload-process.js +!js/jquery.fileupload-ui.js +!js/jquery.fileupload-validate.js +!js/jquery.fileupload-video.js +!js/jquery.fileupload.js +!js/jquery.iframe-transport.js diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/CONTRIBUTING.md b/apps/files/static/files/jQuery-File-Upload-9.21.0/CONTRIBUTING.md new file mode 100644 index 0000000..e182f9b --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/CONTRIBUTING.md @@ -0,0 +1,15 @@ +Please follow these pull request guidelines: + +1. Update your fork to the latest upstream version. + +2. Follow the coding conventions of the original source files (indentation, spaces, brackets layout). + +3. Code changes must pass JSHint validation with the `.jshintrc` settings of this project. + +4. Code changes must pass the QUnit tests defined in the `test` folder. + +5. New features should be covered by accompanying QUnit tests. + +6. Keep your commits as atomic as possible, i.e. create a new commit for every single bug fix or feature added. + +7. Always add meaningful commit messages. diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/LICENSE.txt b/apps/files/static/files/jQuery-File-Upload-9.21.0/LICENSE.txt new file mode 100644 index 0000000..87a6446 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2010 Sebastian Tschan, https://blueimp.net + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/README.md b/apps/files/static/files/jQuery-File-Upload-9.21.0/README.md new file mode 100644 index 0000000..76bdf89 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/README.md @@ -0,0 +1,107 @@ +# jQuery File Upload Plugin + +## Demo +[Demo File Upload](https://blueimp.github.io/jQuery-File-Upload/) + +## Description +File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for jQuery. +Supports cross-domain, chunked and resumable file uploads and client-side image resizing. Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads. + +## Setup +* [How to setup the plugin on your website](https://github.com/blueimp/jQuery-File-Upload/wiki/Setup) +* [How to use only the basic plugin (minimal setup guide).](https://github.com/blueimp/jQuery-File-Upload/wiki/Basic-plugin) + +## Features +* **Multiple file upload:** + Allows to select multiple files at once and upload them simultaneously. +* **Drag & Drop support:** + Allows to upload files by dragging them from your desktop or filemanager and dropping them on your browser window. +* **Upload progress bar:** + Shows a progress bar indicating the upload progress for individual files and for all uploads combined. +* **Cancelable uploads:** + Individual file uploads can be canceled to stop the upload progress. +* **Resumable uploads:** + Aborted uploads can be resumed with browsers supporting the Blob API. +* **Chunked uploads:** + Large files can be uploaded in smaller chunks with browsers supporting the Blob API. +* **Client-side image resizing:** + Images can be automatically resized on client-side with browsers supporting the required JS APIs. +* **Preview images, audio and video:** + A preview of image, audio and video files can be displayed before uploading with browsers supporting the required APIs. +* **No browser plugins (e.g. Adobe Flash) required:** + The implementation is based on open standards like HTML5 and JavaScript and requires no additional browser plugins. +* **Graceful fallback for legacy browsers:** + Uploads files via XMLHttpRequests if supported and uses iframes as fallback for legacy browsers. +* **HTML file upload form fallback:** + Allows progressive enhancement by using a standard HTML file upload form as widget element. +* **Cross-site file uploads:** + Supports uploading files to a different domain with cross-site XMLHttpRequests or iframe redirects. +* **Multiple plugin instances:** + Allows to use multiple plugin instances on the same webpage. +* **Customizable and extensible:** + Provides an API to set individual options and define callback methods for various upload events. +* **Multipart and file contents stream uploads:** + Files can be uploaded as standard "multipart/form-data" or file contents stream (HTTP PUT file upload). +* **Compatible with any server-side application platform:** + Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads. + +## Requirements + +### Mandatory requirements +* [jQuery](https://jquery.com/) v. 1.6+ +* [jQuery UI widget factory](https://api.jqueryui.com/jQuery.widget/) v. 1.9+ (included): Required for the basic File Upload plugin, but very lightweight without any other dependencies from the jQuery UI suite. +* [jQuery Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js) (included): Required for [browsers without XHR file upload support](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). + +### Optional requirements +* [JavaScript Templates engine](https://github.com/blueimp/JavaScript-Templates) v. 2.5.4+: Used to render the selected and uploaded files for the Basic Plus UI and jQuery UI versions. +* [JavaScript Load Image library](https://github.com/blueimp/JavaScript-Load-Image) v. 1.13.0+: Required for the image previews and resizing functionality. +* [JavaScript Canvas to Blob polyfill](https://github.com/blueimp/JavaScript-Canvas-to-Blob) v. 2.1.1+:Required for the image previews and resizing functionality. +* [blueimp Gallery](https://github.com/blueimp/Gallery) v. 2.15.1+: Used to display the uploaded images in a lightbox. +* [Bootstrap](http://getbootstrap.com/) v. 3.2.0+ +* [Glyphicons](http://glyphicons.com/) + +The user interface of all versions, except the jQuery UI version, is built with [Bootstrap](http://getbootstrap.com/) and icons from [Glyphicons](http://glyphicons.com/). + +### Cross-domain requirements +[Cross-domain File Uploads](https://github.com/blueimp/jQuery-File-Upload/wiki/Cross-domain-uploads) using the [Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js) require a redirect back to the origin server to retrieve the upload results. The [example implementation](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/main.js) makes use of [result.html](https://github.com/blueimp/jQuery-File-Upload/blob/master/cors/result.html) as a static redirect page for the origin server. + +The repository also includes the [jQuery XDomainRequest Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/cors/jquery.xdr-transport.js), which enables limited cross-domain AJAX requests in Microsoft Internet Explorer 8 and 9 (IE 10 supports cross-domain XHR requests). +The XDomainRequest object allows GET and POST requests only and doesn't support file uploads. It is used on the [Demo](https://blueimp.github.io/jQuery-File-Upload/) to delete uploaded files from the cross-domain demo file upload service. + +### Custom Backends + +You can add support for various backends by adhering to the specification [outlined here](https://github.com/blueimp/jQuery-File-Upload/wiki/JSON-Response). + +## Browsers + +### Desktop browsers +The File Upload plugin is regularly tested with the latest browser versions and supports the following minimal versions: + +* Google Chrome +* Apple Safari 4.0+ +* Mozilla Firefox 3.0+ +* Opera 11.0+ +* Microsoft Internet Explorer 6.0+ + +### Mobile browsers +The File Upload plugin has been tested with and supports the following mobile browsers: + +* Apple Safari on iOS 6.0+ +* Google Chrome on iOS 6.0+ +* Google Chrome on Android 4.0+ +* Default Browser on Android 2.3+ +* Opera Mobile 12.0+ + +### Supported features +For a detailed overview of the features supported by each browser version, please have a look at the [Extended browser support information](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). + +## Contributing +**Bug fixes** and **new features** can be proposed using [pull requests](https://github.com/blueimp/jQuery-File-Upload/pulls). +Please read the [contribution guidelines](https://github.com/blueimp/jQuery-File-Upload/blob/master/CONTRIBUTING.md) before submitting a pull request. + +## Support +This project is actively maintained, but there is no official support channel. +If you have a question that another developer might help you with, please post to [Stack Overflow](http://stackoverflow.com/questions/tagged/blueimp+jquery+file-upload) and tag your question with `blueimp jquery file upload`. + +## License +Released under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/angularjs.html b/apps/files/static/files/jQuery-File-Upload-9.21.0/angularjs.html new file mode 100644 index 0000000..2051bbf --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/angularjs.html @@ -0,0 +1,211 @@ + + + + + + + +jQuery File Upload Demo - AngularJS version + + + + + + + + + + + + + + + + + + +
+

jQuery File Upload Demo

+

AngularJS version

+ +
+
+

File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for AngularJS.
+ Supports cross-domain, chunked and resumable file uploads and client-side image resizing.
+ Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads.

+
+
+ +
+ + + +
+
+ + + + Add files... + + + + + + +
+ +
+ +
+ +
 
+
+
+ + + + + + + + +
+
+ +
+
+
+

+ + {{file.name}} + {{file.name}} + + {{file.name}} +

+ {{file.error}} +
+

{{file.size | formatFileSize}}

+
+
+ + + +
+
+
+
+
+

Demo Notes

+
+
+
    +
  • The maximum file size for uploads in this demo is 999 KB (default file size is unlimited).
  • +
  • Only image files (JPG, GIF, PNG) are allowed in this demo (by default there is no file type restriction).
  • +
  • Uploaded files will be deleted automatically after 5 minutes or less (demo files are stored in memory).
  • +
  • You can drag & drop files from your desktop on this webpage (see Browser support).
  • +
  • Please refer to the project website and documentation for more information.
  • +
  • Built with the Bootstrap CSS framework and Icons from Glyphicons.
  • +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/basic-plus.html b/apps/files/static/files/jQuery-File-Upload-9.21.0/basic-plus.html new file mode 100644 index 0000000..acee248 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/basic-plus.html @@ -0,0 +1,226 @@ + + + + + + + +jQuery File Upload Demo - Basic Plus version + + + + + + + + + + + +
+

jQuery File Upload Demo

+

Basic Plus version

+ +
+
+

File Upload widget with multiple file selection, drag&drop support, progress bar, validation and preview images, audio and video for jQuery.
+ Supports cross-domain, chunked and resumable file uploads and client-side image resizing.
+ Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads.

+
+
+ + + + Add files... + + + +
+
+ +
+
+
+ +
+
+
+
+

Demo Notes

+
+
+
    +
  • The maximum file size for uploads in this demo is 999 KB (default file size is unlimited).
  • +
  • Only image files (JPG, GIF, PNG) are allowed in this demo (by default there is no file type restriction).
  • +
  • Uploaded files will be deleted automatically after 5 minutes or less (demo files are stored in memory).
  • +
  • You can drag & drop files from your desktop on this webpage (see Browser support).
  • +
  • Please refer to the project website and documentation for more information.
  • +
  • Built with the Bootstrap CSS framework and Icons from Glyphicons.
  • +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/basic.html b/apps/files/static/files/jQuery-File-Upload-9.21.0/basic.html new file mode 100644 index 0000000..232a246 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/basic.html @@ -0,0 +1,136 @@ + + + + + + + +jQuery File Upload Demo - Basic version + + + + + + + + + + + +
+

jQuery File Upload Demo

+

Basic version

+ +
+
+

File Upload widget with multiple file selection, drag&drop support and progress bar for jQuery.
+ Supports cross-domain, chunked and resumable file uploads.
+ Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads.

+
+
+ + + + Select files... + + + +
+
+ +
+
+
+ +
+
+
+
+

Demo Notes

+
+
+
    +
  • The maximum file size for uploads in this demo is 999 KB (default file size is unlimited).
  • +
  • Only image files (JPG, GIF, PNG) are allowed in this demo (by default there is no file type restriction).
  • +
  • Uploaded files will be deleted automatically after 5 minutes or less (demo files are stored in memory).
  • +
  • You can drag & drop files from your desktop on this webpage (see Browser support).
  • +
  • Please refer to the project website and documentation for more information.
  • +
  • Built with the Bootstrap CSS framework and Icons from Glyphicons.
  • +
+
+
+
+ + + + + + + + + + + + diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/bower-version-update.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/bower-version-update.js new file mode 100755 index 0000000..09ce392 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/bower-version-update.js @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +'use strict'; + +var path = require('path'); +var packageJSON = require(path.join(__dirname, 'package.json')); +var bowerFile = path.join(__dirname, 'bower.json'); +var bowerJSON = require('bower-json').parse( + require(bowerFile), + {normalize: true} +); +bowerJSON.version = packageJSON.version; +require('fs').writeFileSync( + bowerFile, + JSON.stringify(bowerJSON, null, 2) + '\n' +); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/bower.json b/apps/files/static/files/jQuery-File-Upload-9.21.0/bower.json new file mode 100644 index 0000000..4b95a32 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/bower.json @@ -0,0 +1,64 @@ +{ + "name": "blueimp-file-upload", + "version": "9.21.0", + "title": "jQuery File Upload", + "description": "File Upload widget with multiple file selection, drag&drop support, progress bar, validation and preview images.", + "keywords": [ + "jquery", + "file", + "upload", + "widget", + "multiple", + "selection", + "drag", + "drop", + "progress", + "preview", + "cross-domain", + "cross-site", + "chunk", + "resume", + "gae", + "go", + "python", + "php", + "bootstrap" + ], + "homepage": "https://github.com/blueimp/jQuery-File-Upload", + "author": { + "name": "Sebastian Tschan", + "url": "https://blueimp.net" + }, + "maintainers": [ + { + "name": "Sebastian Tschan", + "url": "https://blueimp.net" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/blueimp/jQuery-File-Upload.git" + }, + "bugs": "https://github.com/blueimp/jQuery-File-Upload/issues", + "license": "MIT", + "dependencies": { + "jquery": ">=1.6", + "blueimp-tmpl": ">=2.5.4", + "blueimp-load-image": ">=1.13.0", + "blueimp-canvas-to-blob": ">=2.1.1" + }, + "main": [ + "js/jquery.fileupload.js" + ], + "ignore": [ + "/*.*", + "/cors", + "css/jquery-ui-demo-ie8.css", + "css/jquery-ui-demo.css", + "css/style.css", + "js/app.js", + "js/main.js", + "server", + "test" + ] +} diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/cors/postmessage.html b/apps/files/static/files/jQuery-File-Upload-9.21.0/cors/postmessage.html new file mode 100644 index 0000000..6a56cf0 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/cors/postmessage.html @@ -0,0 +1,75 @@ + + + + + +jQuery File Upload Plugin postMessage API + + + + + + diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/cors/result.html b/apps/files/static/files/jQuery-File-Upload-9.21.0/cors/result.html new file mode 100644 index 0000000..e3d6298 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/cors/result.html @@ -0,0 +1,24 @@ + + + + + +jQuery Iframe Transport Plugin Redirect Page + + + + + diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery-ui-demo-ie8.css b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery-ui-demo-ie8.css new file mode 100644 index 0000000..e0e8ea9 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery-ui-demo-ie8.css @@ -0,0 +1,21 @@ +@charset "UTF-8"; +/* + * jQuery File Upload Demo CSS Fixes for IE<9 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.navigation { + list-style: none; + padding: 0; + margin: 1em 0; +} +.navigation li { + display: inline; + margin-right: 10px; +} diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery-ui-demo.css b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery-ui-demo.css new file mode 100644 index 0000000..d7d524d --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery-ui-demo.css @@ -0,0 +1,67 @@ +@charset "UTF-8"; +/* + * jQuery File Upload Demo CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +body { + max-width: 750px; + margin: 0 auto; + padding: 1em; + font-family: "Lucida Grande", "Lucida Sans Unicode", Arial, sans-serif; + font-size: 1em; + line-height: 1.4em; + background: #222; + color: #fff; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +a { + color: orange; + text-decoration: none; +} +img { + border: 0; + vertical-align: middle; +} +h1 { + line-height: 1em; +} +blockquote { + padding: 0 0 0 15px; + margin: 0 0 20px; + border-left: 5px solid #eee; +} +table { + width: 100%; + margin: 10px 0; +} + +.fileupload-progress { + margin: 10px 0; +} +.fileupload-progress .progress-extended { + margin-top: 5px; +} +.error { + color: red; +} + +@media (min-width: 481px) { + .navigation { + list-style: none; + padding: 0; + } + .navigation li { + display: inline-block; + } + .navigation li:not(:first-child):before { + content: "| "; + } +} diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery.fileupload-noscript.css b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery.fileupload-noscript.css new file mode 100644 index 0000000..2409bfb --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery.fileupload-noscript.css @@ -0,0 +1,22 @@ +@charset "UTF-8"; +/* + * jQuery File Upload Plugin NoScript CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.fileinput-button input { + position: static; + opacity: 1; + filter: none; + font-size: inherit !important; + direction: inherit; +} +.fileinput-button span { + display: none; +} diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery.fileupload-ui-noscript.css b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery.fileupload-ui-noscript.css new file mode 100644 index 0000000..30651ac --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery.fileupload-ui-noscript.css @@ -0,0 +1,17 @@ +@charset "UTF-8"; +/* + * jQuery File Upload UI Plugin NoScript CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.fileinput-button i, +.fileupload-buttonbar .delete, +.fileupload-buttonbar .toggle { + display: none; +} diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery.fileupload-ui.css b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery.fileupload-ui.css new file mode 100644 index 0000000..9e36c42 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery.fileupload-ui.css @@ -0,0 +1,57 @@ +@charset "UTF-8"; +/* + * jQuery File Upload UI Plugin CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.fileupload-buttonbar .btn, +.fileupload-buttonbar .toggle { + margin-bottom: 5px; +} +.progress-animated .progress-bar, +.progress-animated .bar { + background: url("../img/progressbar.gif") !important; + filter: none; +} +.fileupload-process { + float: right; + display: none; +} +.fileupload-processing .fileupload-process, +.files .processing .preview { + display: block; + width: 32px; + height: 32px; + background: url("../img/loading.gif") center no-repeat; + background-size: contain; +} +.files audio, +.files video { + max-width: 300px; +} + +@media (max-width: 767px) { + .fileupload-buttonbar .toggle, + .files .toggle, + .files .btn span { + display: none; + } + .files .name { + width: 80px; + word-wrap: break-word; + } + .files audio, + .files video { + max-width: 80px; + } + .files img, + .files canvas { + max-width: 100%; + } +} diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery.fileupload.css b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery.fileupload.css new file mode 100644 index 0000000..8ae3b09 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/jquery.fileupload.css @@ -0,0 +1,37 @@ +@charset "UTF-8"; +/* + * jQuery File Upload Plugin CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.fileinput-button { + position: relative; + overflow: hidden; + display: inline-block; +} +.fileinput-button input { + position: absolute; + top: 0; + right: 0; + margin: 0; + opacity: 0; + -ms-filter: 'alpha(opacity=0)'; + font-size: 200px !important; + direction: ltr; + cursor: pointer; +} + +/* Fixes for IE < 8 */ +@media screen\9 { + .fileinput-button input { + filter: alpha(opacity=0); + font-size: 100%; + height: 100%; + } +} diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/css/style.css b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/style.css new file mode 100644 index 0000000..3aee256 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/css/style.css @@ -0,0 +1,15 @@ +@charset "UTF-8"; +/* + * jQuery File Upload Plugin CSS Example + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +body { + padding-top: 60px; +} diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/img/loading.gif b/apps/files/static/files/jQuery-File-Upload-9.21.0/img/loading.gif new file mode 100644 index 0000000..90f28cb Binary files /dev/null and b/apps/files/static/files/jQuery-File-Upload-9.21.0/img/loading.gif differ diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/img/progressbar.gif b/apps/files/static/files/jQuery-File-Upload-9.21.0/img/progressbar.gif new file mode 100644 index 0000000..fbcce6b Binary files /dev/null and b/apps/files/static/files/jQuery-File-Upload-9.21.0/img/progressbar.gif differ diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/index.html b/apps/files/static/files/jQuery-File-Upload-9.21.0/index.html new file mode 100644 index 0000000..c8c66ad --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/index.html @@ -0,0 +1,255 @@ + + + + + + + +jQuery File Upload Demo + + + + + + + + + + + + + + + + + +
+

jQuery File Upload Demo

+

Basic Plus UI version

+ +
+
+

File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for jQuery.
+ Supports cross-domain, chunked and resumable file uploads and client-side image resizing.
+ Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads.

+
+
+ +
+ + + +
+
+ + + + Add files... + + + + + + + + +
+ +
+ +
+
+
+ +
 
+
+
+ + +
+
+
+
+

Demo Notes

+
+
+
    +
  • The maximum file size for uploads in this demo is 999 KB (default file size is unlimited).
  • +
  • Only image files (JPG, GIF, PNG) are allowed in this demo (by default there is no file type restriction).
  • +
  • Uploaded files will be deleted automatically after 5 minutes or less (demo files are stored in memory).
  • +
  • You can drag & drop files from your desktop on this webpage (see Browser support).
  • +
  • Please refer to the project website and documentation for more information.
  • +
  • Built with the Bootstrap CSS framework and Icons from Glyphicons.
  • +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/jquery-ui.html b/apps/files/static/files/jQuery-File-Upload-9.21.0/jquery-ui.html new file mode 100644 index 0000000..842dd4c --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/jquery-ui.html @@ -0,0 +1,252 @@ + + + + + + + +jQuery File Upload Demo - jQuery UI version + + + + + + + + + + + + + + + + + + + + + +

jQuery File Upload Demo

+

jQuery UI version

+
+ + +
+ +
+

File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for jQuery UI.
+ Supports cross-domain, chunked and resumable file uploads and client-side image resizing.
+ Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads.

+
+ +
+ + + +
+
+ + + Add files... + + + + + + + + +
+ + +
+ +
+
+
+

Demo Notes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/js/app.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/app.js new file mode 100644 index 0000000..e6b7bce --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/app.js @@ -0,0 +1,101 @@ +/* + * jQuery File Upload Plugin Angular JS Example + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global window, angular */ + +;(function () { + 'use strict'; + + var isOnGitHub = window.location.hostname === 'blueimp.github.io', + url = isOnGitHub ? '//jquery-file-upload.appspot.com/' : 'server/php/'; + + angular.module('demo', [ + 'blueimp.fileupload' + ]) + .config([ + '$httpProvider', 'fileUploadProvider', + function ($httpProvider, fileUploadProvider) { + delete $httpProvider.defaults.headers.common['X-Requested-With']; + fileUploadProvider.defaults.redirect = window.location.href.replace( + /\/[^\/]*$/, + '/cors/result.html?%s' + ); + if (isOnGitHub) { + // Demo settings: + angular.extend(fileUploadProvider.defaults, { + // Enable image resizing, except for Android and Opera, + // which actually support image resizing, but fail to + // send Blob objects via XHR requests: + disableImageResize: /Android(?!.*Chrome)|Opera/ + .test(window.navigator.userAgent), + maxFileSize: 999000, + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i + }); + } + } + ]) + + .controller('DemoFileUploadController', [ + '$scope', '$http', '$filter', '$window', + function ($scope, $http) { + $scope.options = { + url: url + }; + if (!isOnGitHub) { + $scope.loadingFiles = true; + $http.get(url) + .then( + function (response) { + $scope.loadingFiles = false; + $scope.queue = response.data.files || []; + }, + function () { + $scope.loadingFiles = false; + } + ); + } + } + ]) + + .controller('FileDestroyController', [ + '$scope', '$http', + function ($scope, $http) { + var file = $scope.file, + state; + if (file.url) { + file.$state = function () { + return state; + }; + file.$destroy = function () { + state = 'pending'; + return $http({ + url: file.deleteUrl, + method: file.deleteType + }).then( + function () { + state = 'resolved'; + $scope.clear(file); + }, + function () { + state = 'rejected'; + } + ); + }; + } else if (!file.$cancel && !file._index) { + file.$cancel = function () { + $scope.clear(file); + }; + } + } + ]); + +}()); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/js/cors/jquery.postmessage-transport.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/cors/jquery.postmessage-transport.js new file mode 100644 index 0000000..2a0c38c --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/cors/jquery.postmessage-transport.js @@ -0,0 +1,126 @@ +/* + * jQuery postMessage Transport Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, require, window, document */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + var counter = 0, + names = [ + 'accepts', + 'cache', + 'contents', + 'contentType', + 'crossDomain', + 'data', + 'dataType', + 'headers', + 'ifModified', + 'mimeType', + 'password', + 'processData', + 'timeout', + 'traditional', + 'type', + 'url', + 'username' + ], + convert = function (p) { + return p; + }; + + $.ajaxSetup({ + converters: { + 'postmessage text': convert, + 'postmessage json': convert, + 'postmessage html': convert + } + }); + + $.ajaxTransport('postmessage', function (options) { + if (options.postMessage && window.postMessage) { + var iframe, + loc = $('').prop('href', options.postMessage)[0], + target = loc.protocol + '//' + loc.host, + xhrUpload = options.xhr().upload; + // IE always includes the port for the host property of a link + // element, but not in the location.host or origin property for the + // default http port 80 and https port 443, so we strip it: + if (/^(http:\/\/.+:80)|(https:\/\/.+:443)$/.test(target)) { + target = target.replace(/:(80|443)$/, ''); + } + return { + send: function (_, completeCallback) { + counter += 1; + var message = { + id: 'postmessage-transport-' + counter + }, + eventName = 'message.' + message.id; + iframe = $( + '' + ).bind('load', function () { + $.each(names, function (i, name) { + message[name] = options[name]; + }); + message.dataType = message.dataType.replace('postmessage ', ''); + $(window).bind(eventName, function (e) { + e = e.originalEvent; + var data = e.data, + ev; + if (e.origin === target && data.id === message.id) { + if (data.type === 'progress') { + ev = document.createEvent('Event'); + ev.initEvent(data.type, false, true); + $.extend(ev, data); + xhrUpload.dispatchEvent(ev); + } else { + completeCallback( + data.status, + data.statusText, + {postmessage: data.result}, + data.headers + ); + iframe.remove(); + $(window).unbind(eventName); + } + } + }); + iframe[0].contentWindow.postMessage( + message, + target + ); + }).appendTo(document.body); + }, + abort: function () { + if (iframe) { + iframe.remove(); + } + } + }; + } + }); + +})); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/js/cors/jquery.xdr-transport.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/cors/jquery.xdr-transport.js new file mode 100644 index 0000000..a4e2699 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/cors/jquery.xdr-transport.js @@ -0,0 +1,89 @@ +/* + * jQuery XDomainRequest Transport Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + * + * Based on Julian Aubourg's ajaxHooks xdr.js: + * https://github.com/jaubourg/ajaxHooks/ + */ + +/* global define, require, window, XDomainRequest */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + if (window.XDomainRequest && !$.support.cors) { + $.ajaxTransport(function (s) { + if (s.crossDomain && s.async) { + if (s.timeout) { + s.xdrTimeout = s.timeout; + delete s.timeout; + } + var xdr; + return { + send: function (headers, completeCallback) { + var addParamChar = /\?/.test(s.url) ? '&' : '?'; + function callback(status, statusText, responses, responseHeaders) { + xdr.onload = xdr.onerror = xdr.ontimeout = $.noop; + xdr = null; + completeCallback(status, statusText, responses, responseHeaders); + } + xdr = new XDomainRequest(); + // XDomainRequest only supports GET and POST: + if (s.type === 'DELETE') { + s.url = s.url + addParamChar + '_method=DELETE'; + s.type = 'POST'; + } else if (s.type === 'PUT') { + s.url = s.url + addParamChar + '_method=PUT'; + s.type = 'POST'; + } else if (s.type === 'PATCH') { + s.url = s.url + addParamChar + '_method=PATCH'; + s.type = 'POST'; + } + xdr.open(s.type, s.url); + xdr.onload = function () { + callback( + 200, + 'OK', + {text: xdr.responseText}, + 'Content-Type: ' + xdr.contentType + ); + }; + xdr.onerror = function () { + callback(404, 'Not Found'); + }; + if (s.xdrTimeout) { + xdr.ontimeout = function () { + callback(0, 'timeout'); + }; + xdr.timeout = s.xdrTimeout; + } + xdr.send((s.hasContent && s.data) || null); + }, + abort: function () { + if (xdr) { + xdr.onerror = $.noop(); + xdr.abort(); + } + } + }; + } + }); + } +})); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-angular.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-angular.js new file mode 100644 index 0000000..1c20552 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-angular.js @@ -0,0 +1,437 @@ +/* + * jQuery File Upload AngularJS Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, angular, require */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'angular', + './jquery.fileupload-image', + './jquery.fileupload-audio', + './jquery.fileupload-video', + './jquery.fileupload-validate' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('angular'), + require('./jquery.fileupload-image'), + require('./jquery.fileupload-audio'), + require('./jquery.fileupload-video'), + require('./jquery.fileupload-validate') + ); + } else { + factory(); + } +}(function () { + 'use strict'; + + angular.module('blueimp.fileupload', []) + + // The fileUpload service provides configuration options + // for the fileUpload directive and default handlers for + // File Upload events: + .provider('fileUpload', function () { + var scopeEvalAsync = function (expression) { + var scope = angular.element(this) + .fileupload('option', 'scope'); + // Schedule a new $digest cycle if not already inside of one + // and evaluate the given expression: + scope.$evalAsync(expression); + }, + addFileMethods = function (scope, data) { + var files = data.files, + file = files[0]; + angular.forEach(files, function (file, index) { + file._index = index; + file.$state = function () { + return data.state(); + }; + file.$processing = function () { + return data.processing(); + }; + file.$progress = function () { + return data.progress(); + }; + file.$response = function () { + return data.response(); + }; + }); + file.$submit = function () { + if (!file.error) { + return data.submit(); + } + }; + file.$cancel = function () { + return data.abort(); + }; + }, + $config; + $config = this.defaults = { + handleResponse: function (e, data) { + var files = data.result && data.result.files; + if (files) { + data.scope.replace(data.files, files); + } else if (data.errorThrown || + data.textStatus === 'error') { + data.files[0].error = data.errorThrown || + data.textStatus; + } + }, + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var scope = data.scope, + filesCopy = []; + angular.forEach(data.files, function (file) { + filesCopy.push(file); + }); + scope.$parent.$applyAsync(function () { + addFileMethods(scope, data); + var method = scope.option('prependFiles') ? + 'unshift' : 'push'; + Array.prototype[method].apply(scope.queue, data.files); + }); + data.process(function () { + return scope.process(data); + }).always(function () { + scope.$parent.$applyAsync(function () { + addFileMethods(scope, data); + scope.replace(filesCopy, data.files); + }); + }).then(function () { + if ((scope.option('autoUpload') || + data.autoUpload) && + data.autoUpload !== false) { + data.submit(); + } + }); + }, + done: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = this; + data.scope.$apply(function () { + data.handleResponse.call(that, e, data); + }); + }, + fail: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = this, + scope = data.scope; + if (data.errorThrown === 'abort') { + scope.clear(data.files); + return; + } + scope.$apply(function () { + data.handleResponse.call(that, e, data); + }); + }, + stop: scopeEvalAsync, + processstart: scopeEvalAsync, + processstop: scopeEvalAsync, + getNumberOfFiles: function () { + var scope = this.scope; + return scope.queue.length - scope.processing(); + }, + dataType: 'json', + autoUpload: false + }; + this.$get = [ + function () { + return { + defaults: $config + }; + } + ]; + }) + + // Format byte numbers to readable presentations: + .provider('formatFileSizeFilter', function () { + var $config = { + // Byte units following the IEC format + // http://en.wikipedia.org/wiki/Kilobyte + units: [ + {size: 1000000000, suffix: ' GB'}, + {size: 1000000, suffix: ' MB'}, + {size: 1000, suffix: ' KB'} + ] + }; + this.defaults = $config; + this.$get = function () { + return function (bytes) { + if (!angular.isNumber(bytes)) { + return ''; + } + var unit = true, + i = 0, + prefix, + suffix; + while (unit) { + unit = $config.units[i]; + prefix = unit.prefix || ''; + suffix = unit.suffix || ''; + if (i === $config.units.length - 1 || bytes >= unit.size) { + return prefix + (bytes / unit.size).toFixed(2) + suffix; + } + i += 1; + } + }; + }; + }) + + // The FileUploadController initializes the fileupload widget and + // provides scope methods to control the File Upload functionality: + .controller('FileUploadController', [ + '$scope', '$element', '$attrs', '$window', 'fileUpload','$q', + function ($scope, $element, $attrs, $window, fileUpload, $q) { + var uploadMethods = { + progress: function () { + return $element.fileupload('progress'); + }, + active: function () { + return $element.fileupload('active'); + }, + option: function (option, data) { + if (arguments.length === 1) { + return $element.fileupload('option', option); + } + $element.fileupload('option', option, data); + }, + add: function (data) { + return $element.fileupload('add', data); + }, + send: function (data) { + return $element.fileupload('send', data); + }, + process: function (data) { + return $element.fileupload('process', data); + }, + processing: function (data) { + return $element.fileupload('processing', data); + } + }; + $scope.disabled = !$window.jQuery.support.fileInput; + $scope.queue = $scope.queue || []; + $scope.clear = function (files) { + var queue = this.queue, + i = queue.length, + file = files, + length = 1; + if (angular.isArray(files)) { + file = files[0]; + length = files.length; + } + while (i) { + i -= 1; + if (queue[i] === file) { + return queue.splice(i, length); + } + } + }; + $scope.replace = function (oldFiles, newFiles) { + var queue = this.queue, + file = oldFiles[0], + i, + j; + for (i = 0; i < queue.length; i += 1) { + if (queue[i] === file) { + for (j = 0; j < newFiles.length; j += 1) { + queue[i + j] = newFiles[j]; + } + return; + } + } + }; + $scope.applyOnQueue = function (method) { + var list = this.queue.slice(0), + i, + file, + promises = []; + for (i = 0; i < list.length; i += 1) { + file = list[i]; + if (file[method]) { + promises.push(file[method]()); + } + } + return $q.all(promises); + }; + $scope.submit = function () { + return this.applyOnQueue('$submit'); + }; + $scope.cancel = function () { + return this.applyOnQueue('$cancel'); + }; + // Add upload methods to the scope: + angular.extend($scope, uploadMethods); + // The fileupload widget will initialize with + // the options provided via "data-"-parameters, + // as well as those given via options object: + $element.fileupload(angular.extend( + {scope: $scope}, + fileUpload.defaults + )).on('fileuploadadd', function (e, data) { + data.scope = $scope; + }).on('fileuploadfail', function (e, data) { + if (data.errorThrown === 'abort') { + return; + } + if (data.dataType && + data.dataType.indexOf('json') === data.dataType.length - 4) { + try { + data.result = angular.fromJson(data.jqXHR.responseText); + } catch (ignore) {} + } + }).on([ + 'fileuploadadd', + 'fileuploadsubmit', + 'fileuploadsend', + 'fileuploaddone', + 'fileuploadfail', + 'fileuploadalways', + 'fileuploadprogress', + 'fileuploadprogressall', + 'fileuploadstart', + 'fileuploadstop', + 'fileuploadchange', + 'fileuploadpaste', + 'fileuploaddrop', + 'fileuploaddragover', + 'fileuploadchunksend', + 'fileuploadchunkdone', + 'fileuploadchunkfail', + 'fileuploadchunkalways', + 'fileuploadprocessstart', + 'fileuploadprocess', + 'fileuploadprocessdone', + 'fileuploadprocessfail', + 'fileuploadprocessalways', + 'fileuploadprocessstop' + ].join(' '), function (e, data) { + $scope.$parent.$applyAsync(function () { + if ($scope.$emit(e.type, data).defaultPrevented) { + e.preventDefault(); + } + }); + }).on('remove', function () { + // Remove upload methods from the scope, + // when the widget is removed: + var method; + for (method in uploadMethods) { + if (uploadMethods.hasOwnProperty(method)) { + delete $scope[method]; + } + } + }); + // Observe option changes: + $scope.$watch( + $attrs.fileUpload, + function (newOptions) { + if (newOptions) { + $element.fileupload('option', newOptions); + } + } + ); + } + ]) + + // Provide File Upload progress feedback: + .controller('FileUploadProgressController', [ + '$scope', '$attrs', '$parse', + function ($scope, $attrs, $parse) { + var fn = $parse($attrs.fileUploadProgress), + update = function () { + var progress = fn($scope); + if (!progress || !progress.total) { + return; + } + $scope.num = Math.floor( + progress.loaded / progress.total * 100 + ); + }; + update(); + $scope.$watch( + $attrs.fileUploadProgress + '.loaded', + function (newValue, oldValue) { + if (newValue !== oldValue) { + update(); + } + } + ); + } + ]) + + // Display File Upload previews: + .controller('FileUploadPreviewController', [ + '$scope', '$element', '$attrs', + function ($scope, $element, $attrs) { + $scope.$watch( + $attrs.fileUploadPreview + '.preview', + function (preview) { + $element.empty(); + if (preview) { + $element.append(preview); + } + } + ); + } + ]) + + .directive('fileUpload', function () { + return { + controller: 'FileUploadController', + scope: true + }; + }) + + .directive('fileUploadProgress', function () { + return { + controller: 'FileUploadProgressController', + scope: true + }; + }) + + .directive('fileUploadPreview', function () { + return { + controller: 'FileUploadPreviewController' + }; + }) + + // Enhance the HTML5 download attribute to + // allow drag&drop of files to the desktop: + .directive('download', function () { + return function (scope, elm) { + elm.on('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [ + 'application/octet-stream', + elm.prop('download'), + elm.prop('href') + ].join(':') + ); + } catch (ignore) {} + }); + }; + }); + +})); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-audio.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-audio.js new file mode 100644 index 0000000..a253776 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-audio.js @@ -0,0 +1,113 @@ +/* + * jQuery File Upload Audio Preview Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, document */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('blueimp-load-image/js/load-image'), + require('./jquery.fileupload-process') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadAudio', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + disabled: '@disableAudioPreview' + }, + { + action: 'setAudio', + name: '@audioPreviewName', + disabled: '@disableAudioPreview' + } + ); + + // The File Upload Audio Preview plugin extends the fileupload widget + // with audio preview functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of audio files to load, + // matched against the file type: + loadAudioFileTypes: /^audio\/.*$/ + }, + + _audioElement: document.createElement('audio'), + + processActions: { + + // Loads the audio file given via data.files and data.index + // as audio element if the browser supports playing it. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadAudio: function (data, options) { + if (options.disabled) { + return data; + } + var file = data.files[data.index], + url, + audio; + if (this._audioElement.canPlayType && + this._audioElement.canPlayType(file.type) && + ($.type(options.maxFileSize) !== 'number' || + file.size <= options.maxFileSize) && + (!options.fileTypes || + options.fileTypes.test(file.type))) { + url = loadImage.createObjectURL(file); + if (url) { + audio = this._audioElement.cloneNode(false); + audio.src = url; + audio.controls = true; + data.audio = audio; + return data; + } + } + return data; + }, + + // Sets the audio element as a property of the file object: + setAudio: function (data, options) { + if (data.audio && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.audio; + } + return data; + } + + } + + }); + +})); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-image.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-image.js new file mode 100644 index 0000000..65fc6d7 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-image.js @@ -0,0 +1,326 @@ +/* + * jQuery File Upload Image Preview & Resize Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, Blob */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + 'load-image-meta', + 'load-image-scale', + 'load-image-exif', + 'canvas-to-blob', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('blueimp-load-image/js/load-image'), + require('blueimp-load-image/js/load-image-meta'), + require('blueimp-load-image/js/load-image-scale'), + require('blueimp-load-image/js/load-image-exif'), + require('blueimp-canvas-to-blob'), + require('./jquery.fileupload-process') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadImageMetaData', + disableImageHead: '@', + disableExif: '@', + disableExifThumbnail: '@', + disableExifSub: '@', + disableExifGps: '@', + disabled: '@disableImageMetaDataLoad' + }, + { + action: 'loadImage', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + noRevoke: '@', + disabled: '@disableImageLoad' + }, + { + action: 'resizeImage', + // Use "image" as prefix for the "@" options: + prefix: 'image', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + forceResize: '@', + disabled: '@disableImageResize' + }, + { + action: 'saveImage', + quality: '@imageQuality', + type: '@imageType', + disabled: '@disableImageResize' + }, + { + action: 'saveImageMetaData', + disabled: '@disableImageMetaDataSave' + }, + { + action: 'resizeImage', + // Use "preview" as prefix for the "@" options: + prefix: 'preview', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + thumbnail: '@', + canvas: '@', + disabled: '@disableImagePreview' + }, + { + action: 'setImage', + name: '@imagePreviewName', + disabled: '@disableImagePreview' + }, + { + action: 'deleteImageReferences', + disabled: '@disableImageReferencesDeletion' + } + ); + + // The File Upload Resize plugin extends the fileupload widget + // with image resize functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of images to load: + // matched against the file type: + loadImageFileTypes: /^image\/(gif|jpeg|png|svg\+xml)$/, + // The maximum file size of images to load: + loadImageMaxFileSize: 10000000, // 10MB + // The maximum width of resized images: + imageMaxWidth: 1920, + // The maximum height of resized images: + imageMaxHeight: 1080, + // Defines the image orientation (1-8) or takes the orientation + // value from Exif data if set to true: + imageOrientation: false, + // Define if resized images should be cropped or only scaled: + imageCrop: false, + // Disable the resize image functionality by default: + disableImageResize: true, + // The maximum width of the preview images: + previewMaxWidth: 80, + // The maximum height of the preview images: + previewMaxHeight: 80, + // Defines the preview orientation (1-8) or takes the orientation + // value from Exif data if set to true: + previewOrientation: true, + // Create the preview using the Exif data thumbnail: + previewThumbnail: true, + // Define if preview images should be cropped or only scaled: + previewCrop: false, + // Define if preview images should be resized as canvas elements: + previewCanvas: true + }, + + processActions: { + + // Loads the image given via data.files and data.index + // as img element, if the browser supports the File API. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadImage: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + dfd = $.Deferred(); + if (($.type(options.maxFileSize) === 'number' && + file.size > options.maxFileSize) || + (options.fileTypes && + !options.fileTypes.test(file.type)) || + !loadImage( + file, + function (img) { + if (img.src) { + data.img = img; + } + dfd.resolveWith(that, [data]); + }, + options + )) { + return data; + } + return dfd.promise(); + }, + + // Resizes the image given as data.canvas or data.img + // and updates data.canvas or data.img with the resized image. + // Also stores the resized image as preview property. + // Accepts the options maxWidth, maxHeight, minWidth, + // minHeight, canvas and crop: + resizeImage: function (data, options) { + if (options.disabled || !(data.canvas || data.img)) { + return data; + } + options = $.extend({canvas: true}, options); + var that = this, + dfd = $.Deferred(), + img = (options.canvas && data.canvas) || data.img, + resolve = function (newImg) { + if (newImg && (newImg.width !== img.width || + newImg.height !== img.height || + options.forceResize)) { + data[newImg.getContext ? 'canvas' : 'img'] = newImg; + } + data.preview = newImg; + dfd.resolveWith(that, [data]); + }, + thumbnail; + if (data.exif) { + if (options.orientation === true) { + options.orientation = data.exif.get('Orientation'); + } + if (options.thumbnail) { + thumbnail = data.exif.get('Thumbnail'); + if (thumbnail) { + loadImage(thumbnail, resolve, options); + return dfd.promise(); + } + } + // Prevent orienting the same image twice: + if (data.orientation) { + delete options.orientation; + } else { + data.orientation = options.orientation; + } + } + if (img) { + resolve(loadImage.scale(img, options)); + return dfd.promise(); + } + return data; + }, + + // Saves the processed image given as data.canvas + // inplace at data.index of data.files: + saveImage: function (data, options) { + if (!data.canvas || options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + dfd = $.Deferred(); + if (data.canvas.toBlob) { + data.canvas.toBlob( + function (blob) { + if (!blob.name) { + if (file.type === blob.type) { + blob.name = file.name; + } else if (file.name) { + blob.name = file.name.replace( + /\.\w+$/, + '.' + blob.type.substr(6) + ); + } + } + // Don't restore invalid meta data: + if (file.type !== blob.type) { + delete data.imageHead; + } + // Store the created blob at the position + // of the original file in the files list: + data.files[data.index] = blob; + dfd.resolveWith(that, [data]); + }, + options.type || file.type, + options.quality + ); + } else { + return data; + } + return dfd.promise(); + }, + + loadImageMetaData: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + dfd = $.Deferred(); + loadImage.parseMetaData(data.files[data.index], function (result) { + $.extend(data, result); + dfd.resolveWith(that, [data]); + }, options); + return dfd.promise(); + }, + + saveImageMetaData: function (data, options) { + if (!(data.imageHead && data.canvas && + data.canvas.toBlob && !options.disabled)) { + return data; + } + var file = data.files[data.index], + blob = new Blob([ + data.imageHead, + // Resized images always have a head size of 20 bytes, + // including the JPEG marker and a minimal JFIF header: + this._blobSlice.call(file, 20) + ], {type: file.type}); + blob.name = file.name; + data.files[data.index] = blob; + return data; + }, + + // Sets the resized version of the image as a property of the + // file object, must be called after "saveImage": + setImage: function (data, options) { + if (data.preview && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.preview; + } + return data; + }, + + deleteImageReferences: function (data, options) { + if (!options.disabled) { + delete data.img; + delete data.canvas; + delete data.preview; + delete data.imageHead; + } + return data; + } + + } + + }); + +})); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-jquery-ui.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-jquery-ui.js new file mode 100644 index 0000000..7b136b3 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-jquery-ui.js @@ -0,0 +1,161 @@ +/* + * jQuery File Upload jQuery UI Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload-ui' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('./jquery.fileupload-ui') + ); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + processdone: function (e, data) { + data.context.find('.start').button('enable'); + }, + progress: function (e, data) { + if (data.context) { + data.context.find('.progress').progressbar( + 'option', + 'value', + parseInt(data.loaded / data.total * 100, 10) + ); + } + }, + progressall: function (e, data) { + var $this = $(this); + $this.find('.fileupload-progress') + .find('.progress').progressbar( + 'option', + 'value', + parseInt(data.loaded / data.total * 100, 10) + ).end() + .find('.progress-extended').each(function () { + $(this).html( + ($this.data('blueimp-fileupload') || + $this.data('fileupload')) + ._renderExtendedProgress(data) + ); + }); + } + }, + + _renderUpload: function (func, files) { + var node = this._super(func, files), + showIconText = $(window).width() > 480; + node.find('.progress').empty().progressbar(); + node.find('.start').button({ + icons: {primary: 'ui-icon-circle-arrow-e'}, + text: showIconText + }); + node.find('.cancel').button({ + icons: {primary: 'ui-icon-cancel'}, + text: showIconText + }); + if (node.hasClass('fade')) { + node.hide(); + } + return node; + }, + + _renderDownload: function (func, files) { + var node = this._super(func, files), + showIconText = $(window).width() > 480; + node.find('.delete').button({ + icons: {primary: 'ui-icon-trash'}, + text: showIconText + }); + if (node.hasClass('fade')) { + node.hide(); + } + return node; + }, + + _startHandler: function (e) { + $(e.currentTarget).button('disable'); + this._super(e); + }, + + _transition: function (node) { + var deferred = $.Deferred(); + if (node.hasClass('fade')) { + node.fadeToggle( + this.options.transitionDuration, + this.options.transitionEasing, + function () { + deferred.resolveWith(node); + } + ); + } else { + deferred.resolveWith(node); + } + return deferred; + }, + + _create: function () { + this._super(); + this.element + .find('.fileupload-buttonbar') + .find('.fileinput-button').each(function () { + var input = $(this).find('input:file').detach(); + $(this) + .button({icons: {primary: 'ui-icon-plusthick'}}) + .append(input); + }) + .end().find('.start') + .button({icons: {primary: 'ui-icon-circle-arrow-e'}}) + .end().find('.cancel') + .button({icons: {primary: 'ui-icon-cancel'}}) + .end().find('.delete') + .button({icons: {primary: 'ui-icon-trash'}}) + .end().find('.progress').progressbar(); + }, + + _destroy: function () { + this.element + .find('.fileupload-buttonbar') + .find('.fileinput-button').each(function () { + var input = $(this).find('input:file').detach(); + $(this) + .button('destroy') + .append(input); + }) + .end().find('.start') + .button('destroy') + .end().find('.cancel') + .button('destroy') + .end().find('.delete') + .button('destroy') + .end().find('.progress').progressbar('destroy'); + this._super(); + } + + }); + +})); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-process.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-process.js new file mode 100644 index 0000000..638f0d2 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-process.js @@ -0,0 +1,178 @@ +/* + * jQuery File Upload Processing Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('./jquery.fileupload') + ); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + var originalAdd = $.blueimp.fileupload.prototype.options.add; + + // The File Upload Processing plugin extends the fileupload widget + // with file processing functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The list of processing actions: + processQueue: [ + /* + { + action: 'log', + type: 'debug' + } + */ + ], + add: function (e, data) { + var $this = $(this); + data.process(function () { + return $this.fileupload('process', data); + }); + originalAdd.call(this, e, data); + } + }, + + processActions: { + /* + log: function (data, options) { + console[options.type]( + 'Processing "' + data.files[data.index].name + '"' + ); + } + */ + }, + + _processFile: function (data, originalData) { + var that = this, + dfd = $.Deferred().resolveWith(that, [data]), + chain = dfd.promise(); + this._trigger('process', null, data); + $.each(data.processQueue, function (i, settings) { + var func = function (data) { + if (originalData.errorThrown) { + return $.Deferred() + .rejectWith(that, [originalData]).promise(); + } + return that.processActions[settings.action].call( + that, + data, + settings + ); + }; + chain = chain.then(func, settings.always && func); + }); + chain + .done(function () { + that._trigger('processdone', null, data); + that._trigger('processalways', null, data); + }) + .fail(function () { + that._trigger('processfail', null, data); + that._trigger('processalways', null, data); + }); + return chain; + }, + + // Replaces the settings of each processQueue item that + // are strings starting with an "@", using the remaining + // substring as key for the option map, + // e.g. "@autoUpload" is replaced with options.autoUpload: + _transformProcessQueue: function (options) { + var processQueue = []; + $.each(options.processQueue, function () { + var settings = {}, + action = this.action, + prefix = this.prefix === true ? action : this.prefix; + $.each(this, function (key, value) { + if ($.type(value) === 'string' && + value.charAt(0) === '@') { + settings[key] = options[ + value.slice(1) || (prefix ? prefix + + key.charAt(0).toUpperCase() + key.slice(1) : key) + ]; + } else { + settings[key] = value; + } + + }); + processQueue.push(settings); + }); + options.processQueue = processQueue; + }, + + // Returns the number of files currently in the processsing queue: + processing: function () { + return this._processing; + }, + + // Processes the files given as files property of the data parameter, + // returns a Promise object that allows to bind callbacks: + process: function (data) { + var that = this, + options = $.extend({}, this.options, data); + if (options.processQueue && options.processQueue.length) { + this._transformProcessQueue(options); + if (this._processing === 0) { + this._trigger('processstart'); + } + $.each(data.files, function (index) { + var opts = index ? $.extend({}, options) : options, + func = function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return that._processFile(opts, data); + }; + opts.index = index; + that._processing += 1; + that._processingQueue = that._processingQueue.then(func, func) + .always(function () { + that._processing -= 1; + if (that._processing === 0) { + that._trigger('processstop'); + } + }); + }); + } + return this._processingQueue; + }, + + _create: function () { + this._super(); + this._processing = 0; + this._processingQueue = $.Deferred().resolveWith(this) + .promise(); + } + + }); + +})); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-ui.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-ui.js new file mode 100644 index 0000000..5058084 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-ui.js @@ -0,0 +1,714 @@ +/* + * jQuery File Upload User Interface Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'blueimp-tmpl', + './jquery.fileupload-image', + './jquery.fileupload-audio', + './jquery.fileupload-video', + './jquery.fileupload-validate' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('blueimp-tmpl'), + require('./jquery.fileupload-image'), + require('./jquery.fileupload-audio'), + require('./jquery.fileupload-video'), + require('./jquery.fileupload-validate') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.tmpl + ); + } +}(function ($, tmpl) { + 'use strict'; + + $.blueimp.fileupload.prototype._specialOptions.push( + 'filesContainer', + 'uploadTemplateId', + 'downloadTemplateId' + ); + + // The UI version extends the file upload widget + // and adds complete user interface interaction: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // By default, files added to the widget are uploaded as soon + // as the user clicks on the start buttons. To enable automatic + // uploads, set the following option to true: + autoUpload: false, + // The ID of the upload template: + uploadTemplateId: 'template-upload', + // The ID of the download template: + downloadTemplateId: 'template-download', + // The container for the list of files. If undefined, it is set to + // an element with class "files" inside of the widget element: + filesContainer: undefined, + // By default, files are appended to the files container. + // Set the following option to true, to prepend files instead: + prependFiles: false, + // The expected data type of the upload response, sets the dataType + // option of the $.ajax upload requests: + dataType: 'json', + + // Error and info messages: + messages: { + unknownError: 'Unknown error' + }, + + // Function returning the current number of files, + // used by the maxNumberOfFiles validation: + getNumberOfFiles: function () { + return this.filesContainer.children() + .not('.processing').length; + }, + + // Callback to retrieve the list of files from the server response: + getFilesFromResponse: function (data) { + if (data.result && $.isArray(data.result.files)) { + return data.result.files; + } + return []; + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop or add API call). + // See the basic file upload widget for more information: + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var $this = $(this), + that = $this.data('blueimp-fileupload') || + $this.data('fileupload'), + options = that.options; + data.context = that._renderUpload(data.files) + .data('data', data) + .addClass('processing'); + options.filesContainer[ + options.prependFiles ? 'prepend' : 'append' + ](data.context); + that._forceReflow(data.context); + that._transition(data.context); + data.process(function () { + return $this.fileupload('process', data); + }).always(function () { + data.context.each(function (index) { + $(this).find('.size').text( + that._formatFileSize(data.files[index].size) + ); + }).removeClass('processing'); + that._renderPreviews(data); + }).done(function () { + data.context.find('.start').prop('disabled', false); + if ((that._trigger('added', e, data) !== false) && + (options.autoUpload || data.autoUpload) && + data.autoUpload !== false) { + data.submit(); + } + }).fail(function () { + if (data.files.error) { + data.context.each(function (index) { + var error = data.files[index].error; + if (error) { + $(this).find('.error').text(error); + } + }); + } + }); + }, + // Callback for the start of each file upload request: + send: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'); + if (data.context && data.dataType && + data.dataType.substr(0, 6) === 'iframe') { + // Iframe Transport does not support progress events. + // In lack of an indeterminate progress bar, we set + // the progress to 100%, showing the full animated bar: + data.context + .find('.progress').addClass( + !$.support.transition && 'progress-animated' + ) + .attr('aria-valuenow', 100) + .children().first().css( + 'width', + '100%' + ); + } + return that._trigger('sent', e, data); + }, + // Callback for successful uploads: + done: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + getFilesFromResponse = data.getFilesFromResponse || + that.options.getFilesFromResponse, + files = getFilesFromResponse(data), + template, + deferred; + if (data.context) { + data.context.each(function (index) { + var file = files[index] || + {error: 'Empty file upload result'}; + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + var node = $(this); + template = that._renderDownload([file]) + .replaceAll(node); + that._forceReflow(template); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + ); + }); + } else { + template = that._renderDownload(files)[ + that.options.prependFiles ? 'prependTo' : 'appendTo' + ](that.options.filesContainer); + that._forceReflow(template); + deferred = that._addFinishedDeferreds(); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + }, + // Callback for failed (abort or error) uploads: + fail: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + template, + deferred; + if (data.context) { + data.context.each(function (index) { + if (data.errorThrown !== 'abort') { + var file = data.files[index]; + file.error = file.error || data.errorThrown || + data.i18n('unknownError'); + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + var node = $(this); + template = that._renderDownload([file]) + .replaceAll(node); + that._forceReflow(template); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + ); + } else { + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + $(this).remove(); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + }); + } else if (data.errorThrown !== 'abort') { + data.context = that._renderUpload(data.files)[ + that.options.prependFiles ? 'prependTo' : 'appendTo' + ](that.options.filesContainer) + .data('data', data); + that._forceReflow(data.context); + deferred = that._addFinishedDeferreds(); + that._transition(data.context).done( + function () { + data.context = $(this); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } else { + that._trigger('failed', e, data); + that._trigger('finished', e, data); + that._addFinishedDeferreds().resolve(); + } + }, + // Callback for upload progress events: + progress: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var progress = Math.floor(data.loaded / data.total * 100); + if (data.context) { + data.context.each(function () { + $(this).find('.progress') + .attr('aria-valuenow', progress) + .children().first().css( + 'width', + progress + '%' + ); + }); + } + }, + // Callback for global upload progress events: + progressall: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var $this = $(this), + progress = Math.floor(data.loaded / data.total * 100), + globalProgressNode = $this.find('.fileupload-progress'), + extendedProgressNode = globalProgressNode + .find('.progress-extended'); + if (extendedProgressNode.length) { + extendedProgressNode.html( + ($this.data('blueimp-fileupload') || $this.data('fileupload')) + ._renderExtendedProgress(data) + ); + } + globalProgressNode + .find('.progress') + .attr('aria-valuenow', progress) + .children().first().css( + 'width', + progress + '%' + ); + }, + // Callback for uploads start, equivalent to the global ajaxStart event: + start: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'); + that._resetFinishedDeferreds(); + that._transition($(this).find('.fileupload-progress')).done( + function () { + that._trigger('started', e); + } + ); + }, + // Callback for uploads stop, equivalent to the global ajaxStop event: + stop: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + deferred = that._addFinishedDeferreds(); + $.when.apply($, that._getFinishedDeferreds()) + .done(function () { + that._trigger('stopped', e); + }); + that._transition($(this).find('.fileupload-progress')).done( + function () { + $(this).find('.progress') + .attr('aria-valuenow', '0') + .children().first().css('width', '0%'); + $(this).find('.progress-extended').html(' '); + deferred.resolve(); + } + ); + }, + processstart: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + $(this).addClass('fileupload-processing'); + }, + processstop: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + $(this).removeClass('fileupload-processing'); + }, + // Callback for file deletion: + destroy: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + removeNode = function () { + that._transition(data.context).done( + function () { + $(this).remove(); + that._trigger('destroyed', e, data); + } + ); + }; + if (data.url) { + data.dataType = data.dataType || that.options.dataType; + $.ajax(data).done(removeNode).fail(function () { + that._trigger('destroyfailed', e, data); + }); + } else { + removeNode(); + } + } + }, + + _resetFinishedDeferreds: function () { + this._finishedUploads = []; + }, + + _addFinishedDeferreds: function (deferred) { + if (!deferred) { + deferred = $.Deferred(); + } + this._finishedUploads.push(deferred); + return deferred; + }, + + _getFinishedDeferreds: function () { + return this._finishedUploads; + }, + + // Link handler, that allows to download files + // by drag & drop of the links to the desktop: + _enableDragToDesktop: function () { + var link = $(this), + url = link.prop('href'), + name = link.prop('download'), + type = 'application/octet-stream'; + link.bind('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [type, name, url].join(':') + ); + } catch (ignore) {} + }); + }, + + _formatFileSize: function (bytes) { + if (typeof bytes !== 'number') { + return ''; + } + if (bytes >= 1000000000) { + return (bytes / 1000000000).toFixed(2) + ' GB'; + } + if (bytes >= 1000000) { + return (bytes / 1000000).toFixed(2) + ' MB'; + } + return (bytes / 1000).toFixed(2) + ' KB'; + }, + + _formatBitrate: function (bits) { + if (typeof bits !== 'number') { + return ''; + } + if (bits >= 1000000000) { + return (bits / 1000000000).toFixed(2) + ' Gbit/s'; + } + if (bits >= 1000000) { + return (bits / 1000000).toFixed(2) + ' Mbit/s'; + } + if (bits >= 1000) { + return (bits / 1000).toFixed(2) + ' kbit/s'; + } + return bits.toFixed(2) + ' bit/s'; + }, + + _formatTime: function (seconds) { + var date = new Date(seconds * 1000), + days = Math.floor(seconds / 86400); + days = days ? days + 'd ' : ''; + return days + + ('0' + date.getUTCHours()).slice(-2) + ':' + + ('0' + date.getUTCMinutes()).slice(-2) + ':' + + ('0' + date.getUTCSeconds()).slice(-2); + }, + + _formatPercentage: function (floatValue) { + return (floatValue * 100).toFixed(2) + ' %'; + }, + + _renderExtendedProgress: function (data) { + return this._formatBitrate(data.bitrate) + ' | ' + + this._formatTime( + (data.total - data.loaded) * 8 / data.bitrate + ) + ' | ' + + this._formatPercentage( + data.loaded / data.total + ) + ' | ' + + this._formatFileSize(data.loaded) + ' / ' + + this._formatFileSize(data.total); + }, + + _renderTemplate: function (func, files) { + if (!func) { + return $(); + } + var result = func({ + files: files, + formatFileSize: this._formatFileSize, + options: this.options + }); + if (result instanceof $) { + return result; + } + return $(this.options.templatesContainer).html(result).children(); + }, + + _renderPreviews: function (data) { + data.context.find('.preview').each(function (index, elm) { + $(elm).append(data.files[index].preview); + }); + }, + + _renderUpload: function (files) { + return this._renderTemplate( + this.options.uploadTemplate, + files + ); + }, + + _renderDownload: function (files) { + return this._renderTemplate( + this.options.downloadTemplate, + files + ).find('a[download]').each(this._enableDragToDesktop).end(); + }, + + _startHandler: function (e) { + e.preventDefault(); + var button = $(e.currentTarget), + template = button.closest('.template-upload'), + data = template.data('data'); + button.prop('disabled', true); + if (data && data.submit) { + data.submit(); + } + }, + + _cancelHandler: function (e) { + e.preventDefault(); + var template = $(e.currentTarget) + .closest('.template-upload,.template-download'), + data = template.data('data') || {}; + data.context = data.context || template; + if (data.abort) { + data.abort(); + } else { + data.errorThrown = 'abort'; + this._trigger('fail', e, data); + } + }, + + _deleteHandler: function (e) { + e.preventDefault(); + var button = $(e.currentTarget); + this._trigger('destroy', e, $.extend({ + context: button.closest('.template-download'), + type: 'DELETE' + }, button.data())); + }, + + _forceReflow: function (node) { + return $.support.transition && node.length && + node[0].offsetWidth; + }, + + _transition: function (node) { + var dfd = $.Deferred(); + if ($.support.transition && node.hasClass('fade') && node.is(':visible')) { + node.bind( + $.support.transition.end, + function (e) { + // Make sure we don't respond to other transitions events + // in the container element, e.g. from button elements: + if (e.target === node[0]) { + node.unbind($.support.transition.end); + dfd.resolveWith(node); + } + } + ).toggleClass('in'); + } else { + node.toggleClass('in'); + dfd.resolveWith(node); + } + return dfd; + }, + + _initButtonBarEventHandlers: function () { + var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), + filesList = this.options.filesContainer; + this._on(fileUploadButtonBar.find('.start'), { + click: function (e) { + e.preventDefault(); + filesList.find('.start').click(); + } + }); + this._on(fileUploadButtonBar.find('.cancel'), { + click: function (e) { + e.preventDefault(); + filesList.find('.cancel').click(); + } + }); + this._on(fileUploadButtonBar.find('.delete'), { + click: function (e) { + e.preventDefault(); + filesList.find('.toggle:checked') + .closest('.template-download') + .find('.delete').click(); + fileUploadButtonBar.find('.toggle') + .prop('checked', false); + } + }); + this._on(fileUploadButtonBar.find('.toggle'), { + change: function (e) { + filesList.find('.toggle').prop( + 'checked', + $(e.currentTarget).is(':checked') + ); + } + }); + }, + + _destroyButtonBarEventHandlers: function () { + this._off( + this.element.find('.fileupload-buttonbar') + .find('.start, .cancel, .delete'), + 'click' + ); + this._off( + this.element.find('.fileupload-buttonbar .toggle'), + 'change.' + ); + }, + + _initEventHandlers: function () { + this._super(); + this._on(this.options.filesContainer, { + 'click .start': this._startHandler, + 'click .cancel': this._cancelHandler, + 'click .delete': this._deleteHandler + }); + this._initButtonBarEventHandlers(); + }, + + _destroyEventHandlers: function () { + this._destroyButtonBarEventHandlers(); + this._off(this.options.filesContainer, 'click'); + this._super(); + }, + + _enableFileInputButton: function () { + this.element.find('.fileinput-button input') + .prop('disabled', false) + .parent().removeClass('disabled'); + }, + + _disableFileInputButton: function () { + this.element.find('.fileinput-button input') + .prop('disabled', true) + .parent().addClass('disabled'); + }, + + _initTemplates: function () { + var options = this.options; + options.templatesContainer = this.document[0].createElement( + options.filesContainer.prop('nodeName') + ); + if (tmpl) { + if (options.uploadTemplateId) { + options.uploadTemplate = tmpl(options.uploadTemplateId); + } + if (options.downloadTemplateId) { + options.downloadTemplate = tmpl(options.downloadTemplateId); + } + } + }, + + _initFilesContainer: function () { + var options = this.options; + if (options.filesContainer === undefined) { + options.filesContainer = this.element.find('.files'); + } else if (!(options.filesContainer instanceof $)) { + options.filesContainer = $(options.filesContainer); + } + }, + + _initSpecialOptions: function () { + this._super(); + this._initFilesContainer(); + this._initTemplates(); + }, + + _create: function () { + this._super(); + this._resetFinishedDeferreds(); + if (!$.support.fileInput) { + this._disableFileInputButton(); + } + }, + + enable: function () { + var wasDisabled = false; + if (this.options.disabled) { + wasDisabled = true; + } + this._super(); + if (wasDisabled) { + this.element.find('input, button').prop('disabled', false); + this._enableFileInputButton(); + } + }, + + disable: function () { + if (!this.options.disabled) { + this.element.find('input, button').prop('disabled', true); + this._disableFileInputButton(); + } + this._super(); + } + + }); + +})); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-validate.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-validate.js new file mode 100644 index 0000000..eebeb37 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-validate.js @@ -0,0 +1,125 @@ +/* + * jQuery File Upload Validation Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, require, window */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('./jquery.fileupload-process') + ); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + // Append to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.push( + { + action: 'validate', + // Always trigger this action, + // even if the previous action was rejected: + always: true, + // Options taken from the global options map: + acceptFileTypes: '@', + maxFileSize: '@', + minFileSize: '@', + maxNumberOfFiles: '@', + disabled: '@disableValidation' + } + ); + + // The File Upload Validation plugin extends the fileupload widget + // with file validation functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + /* + // The regular expression for allowed file types, matches + // against either file type or file name: + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, + // The maximum allowed file size in bytes: + maxFileSize: 10000000, // 10 MB + // The minimum allowed file size in bytes: + minFileSize: undefined, // No minimal file size + // The limit of files to be uploaded: + maxNumberOfFiles: 10, + */ + + // Function returning the current number of files, + // has to be overriden for maxNumberOfFiles validation: + getNumberOfFiles: $.noop, + + // Error and info messages: + messages: { + maxNumberOfFiles: 'Maximum number of files exceeded', + acceptFileTypes: 'File type not allowed', + maxFileSize: 'File is too large', + minFileSize: 'File is too small' + } + }, + + processActions: { + + validate: function (data, options) { + if (options.disabled) { + return data; + } + var dfd = $.Deferred(), + settings = this.options, + file = data.files[data.index], + fileSize; + if (options.minFileSize || options.maxFileSize) { + fileSize = file.size; + } + if ($.type(options.maxNumberOfFiles) === 'number' && + (settings.getNumberOfFiles() || 0) + data.files.length > + options.maxNumberOfFiles) { + file.error = settings.i18n('maxNumberOfFiles'); + } else if (options.acceptFileTypes && + !(options.acceptFileTypes.test(file.type) || + options.acceptFileTypes.test(file.name))) { + file.error = settings.i18n('acceptFileTypes'); + } else if (fileSize > options.maxFileSize) { + file.error = settings.i18n('maxFileSize'); + } else if ($.type(fileSize) === 'number' && + fileSize < options.minFileSize) { + file.error = settings.i18n('minFileSize'); + } else { + delete file.error; + } + if (file.error || data.files.error) { + data.files.error = true; + dfd.rejectWith(this, [data]); + } else { + dfd.resolveWith(this, [data]); + } + return dfd.promise(); + } + + } + + }); + +})); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-video.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-video.js new file mode 100644 index 0000000..aedcec2 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/jquery.fileupload-video.js @@ -0,0 +1,113 @@ +/* + * jQuery File Upload Video Preview Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, document */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('blueimp-load-image/js/load-image'), + require('./jquery.fileupload-process') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadVideo', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + disabled: '@disableVideoPreview' + }, + { + action: 'setVideo', + name: '@videoPreviewName', + disabled: '@disableVideoPreview' + } + ); + + // The File Upload Video Preview plugin extends the fileupload widget + // with video preview functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of video files to load, + // matched against the file type: + loadVideoFileTypes: /^video\/.*$/ + }, + + _videoElement: document.createElement('video'), + + processActions: { + + // Loads the video file given via data.files and data.index + // as video element if the browser supports playing it. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadVideo: function (data, options) { + if (options.disabled) { + return data; + } + var file = data.files[data.index], + url, + video; + if (this._videoElement.canPlayType && + this._videoElement.canPlayType(file.type) && + ($.type(options.maxFileSize) !== 'number' || + file.size <= options.maxFileSize) && + (!options.fileTypes || + options.fileTypes.test(file.type))) { + url = loadImage.createObjectURL(file); + if (url) { + video = this._videoElement.cloneNode(false); + video.src = url; + video.controls = true; + data.video = video; + return data; + } + } + return data; + }, + + // Sets the video element as a property of the file object: + setVideo: function (data, options) { + if (data.video && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.video; + } + return data; + } + + } + + }); + +})); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/js/main.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/main.js new file mode 100644 index 0000000..0403682 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/js/main.js @@ -0,0 +1,75 @@ +/* + * jQuery File Upload Plugin JS Example + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global $, window */ + +$(function () { + 'use strict'; + + // Initialize the jQuery File Upload widget: + $('#fileupload').fileupload({ + // Uncomment the following to send cross-domain cookies: + //xhrFields: {withCredentials: true}, + url: 'server/php/' + }); + + // Enable iframe cross-domain access via redirect option: + $('#fileupload').fileupload( + 'option', + 'redirect', + window.location.href.replace( + /\/[^\/]*$/, + '/cors/result.html?%s' + ) + ); + + if (window.location.hostname === 'blueimp.github.io') { + // Demo settings: + $('#fileupload').fileupload('option', { + url: '//jquery-file-upload.appspot.com/', + // Enable image resizing, except for Android and Opera, + // which actually support image resizing, but fail to + // send Blob objects via XHR requests: + disableImageResize: /Android(?!.*Chrome)|Opera/ + .test(window.navigator.userAgent), + maxFileSize: 999000, + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i + }); + // Upload server status check for browsers with CORS support: + if ($.support.cors) { + $.ajax({ + url: '//jquery-file-upload.appspot.com/', + type: 'HEAD' + }).fail(function () { + $('
') + .text('Upload server currently unavailable - ' + + new Date()) + .appendTo('#fileupload'); + }); + } + } else { + // Load existing files: + $('#fileupload').addClass('fileupload-processing'); + $.ajax({ + // Uncomment the following to send cross-domain cookies: + //xhrFields: {withCredentials: true}, + url: $('#fileupload').fileupload('option', 'url'), + dataType: 'json', + context: $('#fileupload')[0] + }).always(function () { + $(this).removeClass('fileupload-processing'); + }).done(function (result) { + $(this).fileupload('option', 'done') + .call(this, $.Event('done'), {result: result}); + }); + } + +}); diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/package.json b/apps/files/static/files/jQuery-File-Upload-9.21.0/package.json new file mode 100644 index 0000000..11f1fb4 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/package.json @@ -0,0 +1,55 @@ +{ + "name": "blueimp-file-upload", + "version": "9.21.0", + "title": "jQuery File Upload", + "description": "File Upload widget with multiple file selection, drag&drop support, progress bar, validation and preview images, audio and video for jQuery. Supports cross-domain, chunked and resumable file uploads. Works with any server-side platform (Google App Engine, PHP, Python, Ruby on Rails, Java, etc.) that supports standard HTML form file uploads.", + "keywords": [ + "jquery", + "file", + "upload", + "widget", + "multiple", + "selection", + "drag", + "drop", + "progress", + "preview", + "cross-domain", + "cross-site", + "chunk", + "resume", + "gae", + "go", + "python", + "php", + "bootstrap" + ], + "homepage": "https://github.com/blueimp/jQuery-File-Upload", + "author": { + "name": "Sebastian Tschan", + "url": "https://blueimp.net" + }, + "repository": { + "type": "git", + "url": "git://github.com/blueimp/jQuery-File-Upload.git" + }, + "license": "MIT", + "optionalDependencies": { + "blueimp-canvas-to-blob": "3.5.0", + "blueimp-load-image": "2.12.2", + "blueimp-tmpl": "3.6.0" + }, + "devDependencies": { + "bower-json": "0.8.1", + "jshint": "2.9.3" + }, + "scripts": { + "bower-version-update": "./bower-version-update.js", + "lint": "jshint *.js js/*.js js/cors/*.js", + "test": "npm run lint", + "preversion": "npm test", + "version": "npm run bower-version-update && git add bower.json", + "postversion": "git push --tags origin master && npm publish" + }, + "main": "js/jquery.fileupload.js" +} diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-go/app.yaml b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-go/app.yaml new file mode 100644 index 0000000..2d09daa --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-go/app.yaml @@ -0,0 +1,12 @@ +application: jquery-file-upload +version: 2 +runtime: go +api_version: go1 + +handlers: +- url: /(favicon\.ico|robots\.txt) + static_files: static/\1 + upload: static/(.*) + expiration: '1d' +- url: /.* + script: _go_app diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-go/app/main.go b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-go/app/main.go new file mode 100644 index 0000000..a92d128 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-go/app/main.go @@ -0,0 +1,361 @@ +/* + * jQuery File Upload Plugin GAE Go Example + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +package app + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "github.com/disintegration/gift" + "golang.org/x/net/context" + "google.golang.org/appengine" + "google.golang.org/appengine/memcache" + "hash/crc32" + "image" + "image/gif" + "image/jpeg" + "image/png" + "io" + "log" + "mime/multipart" + "net/http" + "net/url" + "path/filepath" + "regexp" + "strings" +) + +const ( + WEBSITE = "https://blueimp.github.io/jQuery-File-Upload/" + MIN_FILE_SIZE = 1 // bytes + // Max file size is memcache limit (1MB) minus key size minus overhead: + MAX_FILE_SIZE = 999000 // bytes + IMAGE_TYPES = "image/(gif|p?jpeg|(x-)?png)" + ACCEPT_FILE_TYPES = IMAGE_TYPES + THUMB_MAX_WIDTH = 80 + THUMB_MAX_HEIGHT = 80 + EXPIRATION_TIME = 300 // seconds + // If empty, only allow redirects to the referer protocol+host. + // Set to a regexp string for custom pattern matching: + REDIRECT_ALLOW_TARGET = "" +) + +var ( + imageTypes = regexp.MustCompile(IMAGE_TYPES) + acceptFileTypes = regexp.MustCompile(ACCEPT_FILE_TYPES) + thumbSuffix = "." + fmt.Sprint(THUMB_MAX_WIDTH) + "x" + + fmt.Sprint(THUMB_MAX_HEIGHT) +) + +func escape(s string) string { + return strings.Replace(url.QueryEscape(s), "+", "%20", -1) +} + +func extractKey(r *http.Request) string { + // Use RequestURI instead of r.URL.Path, as we need the encoded form: + path := strings.Split(r.RequestURI, "?")[0] + // Also adjust double encoded slashes: + return strings.Replace(path[1:], "%252F", "%2F", -1) +} + +func check(err error) { + if err != nil { + panic(err) + } +} + +type FileInfo struct { + Key string `json:"-"` + ThumbnailKey string `json:"-"` + Url string `json:"url,omitempty"` + ThumbnailUrl string `json:"thumbnailUrl,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Size int64 `json:"size"` + Error string `json:"error,omitempty"` + DeleteUrl string `json:"deleteUrl,omitempty"` + DeleteType string `json:"deleteType,omitempty"` +} + +func (fi *FileInfo) ValidateType() (valid bool) { + if acceptFileTypes.MatchString(fi.Type) { + return true + } + fi.Error = "Filetype not allowed" + return false +} + +func (fi *FileInfo) ValidateSize() (valid bool) { + if fi.Size < MIN_FILE_SIZE { + fi.Error = "File is too small" + } else if fi.Size > MAX_FILE_SIZE { + fi.Error = "File is too big" + } else { + return true + } + return false +} + +func (fi *FileInfo) CreateUrls(r *http.Request, c context.Context) { + u := &url.URL{ + Scheme: r.URL.Scheme, + Host: appengine.DefaultVersionHostname(c), + Path: "/", + } + uString := u.String() + fi.Url = uString + fi.Key + fi.DeleteUrl = fi.Url + fi.DeleteType = "DELETE" + if fi.ThumbnailKey != "" { + fi.ThumbnailUrl = uString + fi.ThumbnailKey + } +} + +func (fi *FileInfo) SetKey(checksum uint32) { + fi.Key = escape(string(fi.Type)) + "/" + + escape(fmt.Sprint(checksum)) + "/" + + escape(string(fi.Name)) +} + +func (fi *FileInfo) createThumb(buffer *bytes.Buffer, c context.Context) { + if imageTypes.MatchString(fi.Type) { + src, _, err := image.Decode(bytes.NewReader(buffer.Bytes())) + check(err) + filter := gift.New(gift.ResizeToFit( + THUMB_MAX_WIDTH, + THUMB_MAX_HEIGHT, + gift.LanczosResampling, + )) + dst := image.NewNRGBA(filter.Bounds(src.Bounds())) + filter.Draw(dst, src) + buffer.Reset() + bWriter := bufio.NewWriter(buffer) + switch fi.Type { + case "image/jpeg", "image/pjpeg": + err = jpeg.Encode(bWriter, dst, nil) + case "image/gif": + err = gif.Encode(bWriter, dst, nil) + default: + err = png.Encode(bWriter, dst) + } + check(err) + bWriter.Flush() + thumbnailKey := fi.Key + thumbSuffix + filepath.Ext(fi.Name) + item := &memcache.Item{ + Key: thumbnailKey, + Value: buffer.Bytes(), + } + err = memcache.Set(c, item) + check(err) + fi.ThumbnailKey = thumbnailKey + } +} + +func handleUpload(r *http.Request, p *multipart.Part) (fi *FileInfo) { + fi = &FileInfo{ + Name: p.FileName(), + Type: p.Header.Get("Content-Type"), + } + if !fi.ValidateType() { + return + } + defer func() { + if rec := recover(); rec != nil { + log.Println(rec) + fi.Error = rec.(error).Error() + } + }() + var buffer bytes.Buffer + hash := crc32.NewIEEE() + mw := io.MultiWriter(&buffer, hash) + lr := &io.LimitedReader{R: p, N: MAX_FILE_SIZE + 1} + _, err := io.Copy(mw, lr) + check(err) + fi.Size = MAX_FILE_SIZE + 1 - lr.N + if !fi.ValidateSize() { + return + } + fi.SetKey(hash.Sum32()) + item := &memcache.Item{ + Key: fi.Key, + Value: buffer.Bytes(), + } + context := appengine.NewContext(r) + err = memcache.Set(context, item) + check(err) + fi.createThumb(&buffer, context) + fi.CreateUrls(r, context) + return +} + +func getFormValue(p *multipart.Part) string { + var b bytes.Buffer + io.CopyN(&b, p, int64(1<<20)) // Copy max: 1 MiB + return b.String() +} + +func handleUploads(r *http.Request) (fileInfos []*FileInfo) { + fileInfos = make([]*FileInfo, 0) + mr, err := r.MultipartReader() + check(err) + r.Form, err = url.ParseQuery(r.URL.RawQuery) + check(err) + part, err := mr.NextPart() + for err == nil { + if name := part.FormName(); name != "" { + if part.FileName() != "" { + fileInfos = append(fileInfos, handleUpload(r, part)) + } else { + r.Form[name] = append(r.Form[name], getFormValue(part)) + } + } + part, err = mr.NextPart() + } + return +} + +func validateRedirect(r *http.Request, redirect string) bool { + if redirect != "" { + var redirectAllowTarget *regexp.Regexp + if REDIRECT_ALLOW_TARGET != "" { + redirectAllowTarget = regexp.MustCompile(REDIRECT_ALLOW_TARGET) + } else { + referer := r.Referer() + if referer == "" { + return false + } + refererUrl, err := url.Parse(referer) + if err != nil { + return false + } + redirectAllowTarget = regexp.MustCompile("^" + regexp.QuoteMeta( + refererUrl.Scheme+"://"+refererUrl.Host+"/", + )) + } + return redirectAllowTarget.MatchString(redirect) + } + return false +} + +func get(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + http.Redirect(w, r, WEBSITE, http.StatusFound) + return + } + // Use RequestURI instead of r.URL.Path, as we need the encoded form: + key := extractKey(r) + parts := strings.Split(key, "/") + if len(parts) == 3 { + context := appengine.NewContext(r) + item, err := memcache.Get(context, key) + if err == nil { + w.Header().Add("X-Content-Type-Options", "nosniff") + contentType, _ := url.QueryUnescape(parts[0]) + if !imageTypes.MatchString(contentType) { + contentType = "application/octet-stream" + } + w.Header().Add("Content-Type", contentType) + w.Header().Add( + "Cache-Control", + fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME), + ) + w.Write(item.Value) + return + } + } + http.Error(w, "404 Not Found", http.StatusNotFound) +} + +func post(w http.ResponseWriter, r *http.Request) { + result := make(map[string][]*FileInfo, 1) + result["files"] = handleUploads(r) + b, err := json.Marshal(result) + check(err) + if redirect := r.FormValue("redirect"); validateRedirect(r, redirect) { + if strings.Contains(redirect, "%s") { + redirect = fmt.Sprintf( + redirect, + escape(string(b)), + ) + } + http.Redirect(w, r, redirect, http.StatusFound) + return + } + w.Header().Set("Cache-Control", "no-cache") + jsonType := "application/json" + if strings.Index(r.Header.Get("Accept"), jsonType) != -1 { + w.Header().Set("Content-Type", jsonType) + } + fmt.Fprintln(w, string(b)) +} + +func delete(w http.ResponseWriter, r *http.Request) { + key := extractKey(r) + parts := strings.Split(key, "/") + if len(parts) == 3 { + result := make(map[string]bool, 1) + context := appengine.NewContext(r) + err := memcache.Delete(context, key) + if err == nil { + result[key] = true + contentType, _ := url.QueryUnescape(parts[0]) + if imageTypes.MatchString(contentType) { + thumbnailKey := key + thumbSuffix + filepath.Ext(parts[2]) + err := memcache.Delete(context, thumbnailKey) + if err == nil { + result[thumbnailKey] = true + } + } + } + w.Header().Set("Content-Type", "application/json") + b, err := json.Marshal(result) + check(err) + fmt.Fprintln(w, string(b)) + } else { + http.Error(w, "405 Method not allowed", http.StatusMethodNotAllowed) + } +} + +func handle(w http.ResponseWriter, r *http.Request) { + params, err := url.ParseQuery(r.URL.RawQuery) + check(err) + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add( + "Access-Control-Allow-Methods", + "OPTIONS, HEAD, GET, POST, DELETE", + ) + w.Header().Add( + "Access-Control-Allow-Headers", + "Content-Type, Content-Range, Content-Disposition", + ) + switch r.Method { + case "OPTIONS", "HEAD": + return + case "GET": + get(w, r) + case "POST": + if len(params["_method"]) > 0 && params["_method"][0] == "DELETE" { + delete(w, r) + } else { + post(w, r) + } + case "DELETE": + delete(w, r) + default: + http.Error(w, "501 Not Implemented", http.StatusNotImplemented) + } +} + +func init() { + http.HandleFunc("/", handle) +} diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-go/static/favicon.ico b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-go/static/favicon.ico new file mode 100644 index 0000000..1a71ea7 Binary files /dev/null and b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-go/static/favicon.ico differ diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-go/static/robots.txt b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-go/static/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-go/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-python/app.yaml b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-python/app.yaml new file mode 100644 index 0000000..764449b --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-python/app.yaml @@ -0,0 +1,17 @@ +application: jquery-file-upload +version: 1 +runtime: python27 +api_version: 1 +threadsafe: true + +libraries: +- name: PIL + version: latest + +handlers: +- url: /(favicon\.ico|robots\.txt) + static_files: static/\1 + upload: static/(.*) + expiration: '1d' +- url: /.* + script: main.app diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-python/main.py b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-python/main.py new file mode 100644 index 0000000..1955ac0 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-python/main.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +# +# jQuery File Upload Plugin GAE Python Example +# https://github.com/blueimp/jQuery-File-Upload +# +# Copyright 2011, Sebastian Tschan +# https://blueimp.net +# +# Licensed under the MIT license: +# https://opensource.org/licenses/MIT +# + +from google.appengine.api import memcache, images +import json +import os +import re +import urllib +import webapp2 + +DEBUG=os.environ.get('SERVER_SOFTWARE', '').startswith('Dev') +WEBSITE = 'https://blueimp.github.io/jQuery-File-Upload/' +MIN_FILE_SIZE = 1 # bytes +# Max file size is memcache limit (1MB) minus key size minus overhead: +MAX_FILE_SIZE = 999000 # bytes +IMAGE_TYPES = re.compile('image/(gif|p?jpeg|(x-)?png)') +ACCEPT_FILE_TYPES = IMAGE_TYPES +THUMB_MAX_WIDTH = 80 +THUMB_MAX_HEIGHT = 80 +THUMB_SUFFIX = '.'+str(THUMB_MAX_WIDTH)+'x'+str(THUMB_MAX_HEIGHT)+'.png' +EXPIRATION_TIME = 300 # seconds +# If set to None, only allow redirects to the referer protocol+host. +# Set to a regexp for custom pattern matching against the redirect value: +REDIRECT_ALLOW_TARGET = None + +class CORSHandler(webapp2.RequestHandler): + def cors(self): + headers = self.response.headers + headers['Access-Control-Allow-Origin'] = '*' + headers['Access-Control-Allow-Methods'] =\ + 'OPTIONS, HEAD, GET, POST, DELETE' + headers['Access-Control-Allow-Headers'] =\ + 'Content-Type, Content-Range, Content-Disposition' + + def initialize(self, request, response): + super(CORSHandler, self).initialize(request, response) + self.cors() + + def json_stringify(self, obj): + return json.dumps(obj, separators=(',', ':')) + + def options(self, *args, **kwargs): + pass + +class UploadHandler(CORSHandler): + def validate(self, file): + if file['size'] < MIN_FILE_SIZE: + file['error'] = 'File is too small' + elif file['size'] > MAX_FILE_SIZE: + file['error'] = 'File is too big' + elif not ACCEPT_FILE_TYPES.match(file['type']): + file['error'] = 'Filetype not allowed' + else: + return True + return False + + def validate_redirect(self, redirect): + if redirect: + if REDIRECT_ALLOW_TARGET: + return REDIRECT_ALLOW_TARGET.match(redirect) + referer = self.request.headers['referer'] + if referer: + from urlparse import urlparse + parts = urlparse(referer) + redirect_allow_target = '^' + re.escape( + parts.scheme + '://' + parts.netloc + '/' + ) + return re.match(redirect_allow_target, redirect) + return False + + def get_file_size(self, file): + file.seek(0, 2) # Seek to the end of the file + size = file.tell() # Get the position of EOF + file.seek(0) # Reset the file position to the beginning + return size + + def write_blob(self, data, info): + key = urllib.quote(info['type'].encode('utf-8'), '') +\ + '/' + str(hash(data)) +\ + '/' + urllib.quote(info['name'].encode('utf-8'), '') + try: + memcache.set(key, data, time=EXPIRATION_TIME) + except: #Failed to add to memcache + return (None, None) + thumbnail_key = None + if IMAGE_TYPES.match(info['type']): + try: + img = images.Image(image_data=data) + img.resize( + width=THUMB_MAX_WIDTH, + height=THUMB_MAX_HEIGHT + ) + thumbnail_data = img.execute_transforms() + thumbnail_key = key + THUMB_SUFFIX + memcache.set( + thumbnail_key, + thumbnail_data, + time=EXPIRATION_TIME + ) + except: #Failed to resize Image or add to memcache + thumbnail_key = None + return (key, thumbnail_key) + + def handle_upload(self): + results = [] + for name, fieldStorage in self.request.POST.items(): + if type(fieldStorage) is unicode: + continue + result = {} + result['name'] = urllib.unquote(fieldStorage.filename) + result['type'] = fieldStorage.type + result['size'] = self.get_file_size(fieldStorage.file) + if self.validate(result): + key, thumbnail_key = self.write_blob( + fieldStorage.value, + result + ) + if key is not None: + result['url'] = self.request.host_url + '/' + key + result['deleteUrl'] = result['url'] + result['deleteType'] = 'DELETE' + if thumbnail_key is not None: + result['thumbnailUrl'] = self.request.host_url +\ + '/' + thumbnail_key + else: + result['error'] = 'Failed to store uploaded file.' + results.append(result) + return results + + def head(self): + pass + + def get(self): + self.redirect(WEBSITE) + + def post(self): + if (self.request.get('_method') == 'DELETE'): + return self.delete() + result = {'files': self.handle_upload()} + s = self.json_stringify(result) + redirect = self.request.get('redirect') + if self.validate_redirect(redirect): + return self.redirect(str( + redirect.replace('%s', urllib.quote(s, ''), 1) + )) + if 'application/json' in self.request.headers.get('Accept'): + self.response.headers['Content-Type'] = 'application/json' + self.response.write(s) + +class FileHandler(CORSHandler): + def normalize(self, str): + return urllib.quote(urllib.unquote(str), '') + + def get(self, content_type, data_hash, file_name): + content_type = self.normalize(content_type) + file_name = self.normalize(file_name) + key = content_type + '/' + data_hash + '/' + file_name + data = memcache.get(key) + if data is None: + return self.error(404) + # Prevent browsers from MIME-sniffing the content-type: + self.response.headers['X-Content-Type-Options'] = 'nosniff' + content_type = urllib.unquote(content_type) + if not IMAGE_TYPES.match(content_type): + # Force a download dialog for non-image types: + content_type = 'application/octet-stream' + elif file_name.endswith(THUMB_SUFFIX): + content_type = 'image/png' + self.response.headers['Content-Type'] = content_type + # Cache for the expiration time: + self.response.headers['Cache-Control'] = 'public,max-age=%d' \ + % EXPIRATION_TIME + self.response.write(data) + + def delete(self, content_type, data_hash, file_name): + content_type = self.normalize(content_type) + file_name = self.normalize(file_name) + key = content_type + '/' + data_hash + '/' + file_name + result = {key: memcache.delete(key)} + content_type = urllib.unquote(content_type) + if IMAGE_TYPES.match(content_type): + thumbnail_key = key + THUMB_SUFFIX + result[thumbnail_key] = memcache.delete(thumbnail_key) + if 'application/json' in self.request.headers.get('Accept'): + self.response.headers['Content-Type'] = 'application/json' + s = self.json_stringify(result) + self.response.write(s) + +app = webapp2.WSGIApplication( + [ + ('/', UploadHandler), + ('/(.+)/([^/]+)/([^/]+)', FileHandler) + ], + debug=DEBUG +) diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-python/static/favicon.ico b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-python/static/favicon.ico new file mode 100644 index 0000000..1a71ea7 Binary files /dev/null and b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-python/static/favicon.ico differ diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-python/static/robots.txt b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-python/static/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/gae-python/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/Dockerfile b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/Dockerfile new file mode 100644 index 0000000..8633fee --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/Dockerfile @@ -0,0 +1,38 @@ +FROM php:7-apache + +# Enable the Apache Headers module: +RUN ln -s /etc/apache2/mods-available/headers.load \ + /etc/apache2/mods-enabled/headers.load + +# Enable the Apache Rewrite module: +RUN ln -s /etc/apache2/mods-available/rewrite.load \ + /etc/apache2/mods-enabled/rewrite.load + +# Install GD, Imagick and ImageMagick as image conversion options: +RUN DEBIAN_FRONTEND=noninteractive \ + apt-get update && apt-get install -y --no-install-recommends \ + libpng-dev \ + libjpeg-dev \ + libmagickwand-dev \ + imagemagick \ + && pecl install \ + imagick \ + && docker-php-ext-enable \ + imagick \ + && docker-php-ext-configure \ + gd --with-jpeg-dir=/usr/include/ \ + && docker-php-ext-install \ + gd \ + # Uninstall obsolete packages: + && apt-get autoremove -y \ + libpng-dev \ + libjpeg-dev \ + libmagickwand-dev \ + # Remove obsolete files: + && apt-get clean \ + && rm -rf \ + /tmp/* \ + /usr/share/doc/* \ + /var/cache/* \ + /var/lib/apt/lists/* \ + /var/tmp/* diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/UploadHandler.php b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/UploadHandler.php new file mode 100755 index 0000000..285d046 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/UploadHandler.php @@ -0,0 +1,1411 @@ + 'The uploaded file exceeds the upload_max_filesize directive in php.ini', + 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', + 3 => 'The uploaded file was only partially uploaded', + 4 => 'No file was uploaded', + 6 => 'Missing a temporary folder', + 7 => 'Failed to write file to disk', + 8 => 'A PHP extension stopped the file upload', + 'post_max_size' => 'The uploaded file exceeds the post_max_size directive in php.ini', + 'max_file_size' => 'File is too big', + 'min_file_size' => 'File is too small', + 'accept_file_types' => 'Filetype not allowed', + 'max_number_of_files' => 'Maximum number of files exceeded', + 'max_width' => 'Image exceeds maximum width', + 'min_width' => 'Image requires a minimum width', + 'max_height' => 'Image exceeds maximum height', + 'min_height' => 'Image requires a minimum height', + 'abort' => 'File upload aborted', + 'image_resize' => 'Failed to resize image' + ); + + protected $image_objects = array(); + + public function __construct($options = null, $initialize = true, $error_messages = null) { + $this->response = array(); + $this->options = array( + 'script_url' => $this->get_full_url().'/'.$this->basename($this->get_server_var('SCRIPT_NAME')), + 'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/files/', + 'upload_url' => $this->get_full_url().'/files/', + 'input_stream' => 'php://input', + 'user_dirs' => false, + 'mkdir_mode' => 0755, + 'param_name' => 'files', + // Set the following option to 'POST', if your server does not support + // DELETE requests. This is a parameter sent to the client: + 'delete_type' => 'DELETE', + 'access_control_allow_origin' => '*', + 'access_control_allow_credentials' => false, + 'access_control_allow_methods' => array( + 'OPTIONS', + 'HEAD', + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE' + ), + 'access_control_allow_headers' => array( + 'Content-Type', + 'Content-Range', + 'Content-Disposition' + ), + // By default, allow redirects to the referer protocol+host: + 'redirect_allow_target' => '/^'.preg_quote( + parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_SCHEME) + .'://' + .parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_HOST) + .'/', // Trailing slash to not match subdomains by mistake + '/' // preg_quote delimiter param + ).'/', + // Enable to provide file downloads via GET requests to the PHP script: + // 1. Set to 1 to download files via readfile method through PHP + // 2. Set to 2 to send a X-Sendfile header for lighttpd/Apache + // 3. Set to 3 to send a X-Accel-Redirect header for nginx + // If set to 2 or 3, adjust the upload_url option to the base path of + // the redirect parameter, e.g. '/files/'. + 'download_via_php' => false, + // Read files in chunks to avoid memory limits when download_via_php + // is enabled, set to 0 to disable chunked reading of files: + 'readfile_chunk_size' => 10 * 1024 * 1024, // 10 MiB + // Defines which files can be displayed inline when downloaded: + 'inline_file_types' => '/\.(gif|jpe?g|png)$/i', + // Defines which files (based on their names) are accepted for upload: + 'accept_file_types' => '/.+$/i', + // The php.ini settings upload_max_filesize and post_max_size + // take precedence over the following max_file_size setting: + 'max_file_size' => null, + 'min_file_size' => 1, + // The maximum number of files for the upload directory: + 'max_number_of_files' => null, + // Defines which files are handled as image files: + 'image_file_types' => '/\.(gif|jpe?g|png)$/i', + // Use exif_imagetype on all files to correct file extensions: + 'correct_image_extensions' => false, + // Image resolution restrictions: + 'max_width' => null, + 'max_height' => null, + 'min_width' => 1, + 'min_height' => 1, + // Set the following option to false to enable resumable uploads: + 'discard_aborted_uploads' => true, + // Set to 0 to use the GD library to scale and orient images, + // set to 1 to use imagick (if installed, falls back to GD), + // set to 2 to use the ImageMagick convert binary directly: + 'image_library' => 1, + // Uncomment the following to define an array of resource limits + // for imagick: + /* + 'imagick_resource_limits' => array( + imagick::RESOURCETYPE_MAP => 32, + imagick::RESOURCETYPE_MEMORY => 32 + ), + */ + // Command or path for to the ImageMagick convert binary: + 'convert_bin' => 'convert', + // Uncomment the following to add parameters in front of each + // ImageMagick convert call (the limit constraints seem only + // to have an effect if put in front): + /* + 'convert_params' => '-limit memory 32MiB -limit map 32MiB', + */ + // Command or path for to the ImageMagick identify binary: + 'identify_bin' => 'identify', + 'image_versions' => array( + // The empty image version key defines options for the original image. + // Keep in mind: these image manipulations are inherited by all other image versions from this point onwards. + // Also note that the property 'no_cache' is not inherited, since it's not a manipulation. + '' => array( + // Automatically rotate images based on EXIF meta data: + 'auto_orient' => true + ), + // You can add arrays to generate different versions. + // The name of the key is the name of the version (example: 'medium'). + // the array contains the options to apply. + /* + 'medium' => array( + 'max_width' => 800, + 'max_height' => 600 + ), + */ + 'thumbnail' => array( + // Uncomment the following to use a defined directory for the thumbnails + // instead of a subdirectory based on the version identifier. + // Make sure that this directory doesn't allow execution of files if you + // don't pose any restrictions on the type of uploaded files, e.g. by + // copying the .htaccess file from the files directory for Apache: + //'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/thumb/', + //'upload_url' => $this->get_full_url().'/thumb/', + // Uncomment the following to force the max + // dimensions and e.g. create square thumbnails: + // 'auto_orient' => true, + // 'crop' => true, + // 'jpeg_quality' => 70, + // 'no_cache' => true, (there's a caching option, but this remembers thumbnail sizes from a previous action!) + // 'strip' => true, (this strips EXIF tags, such as geolocation) + 'max_width' => 80, // either specify width, or set to 0. Then width is automatically adjusted - keeping aspect ratio to a specified max_height. + 'max_height' => 80 // either specify height, or set to 0. Then height is automatically adjusted - keeping aspect ratio to a specified max_width. + ) + ), + 'print_response' => true + ); + if ($options) { + $this->options = $options + $this->options; + } + if ($error_messages) { + $this->error_messages = $error_messages + $this->error_messages; + } + if ($initialize) { + $this->initialize(); + } + } + + protected function initialize() { + switch ($this->get_server_var('REQUEST_METHOD')) { + case 'OPTIONS': + case 'HEAD': + $this->head(); + break; + case 'GET': + $this->get($this->options['print_response']); + break; + case 'PATCH': + case 'PUT': + case 'POST': + $this->post($this->options['print_response']); + break; + case 'DELETE': + $this->delete($this->options['print_response']); + break; + default: + $this->header('HTTP/1.1 405 Method Not Allowed'); + } + } + + protected function get_full_url() { + $https = !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'on') === 0 || + !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && + strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0; + return + ($https ? 'https://' : 'http://'). + (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : ''). + (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME']. + ($https && $_SERVER['SERVER_PORT'] === 443 || + $_SERVER['SERVER_PORT'] === 80 ? '' : ':'.$_SERVER['SERVER_PORT']))). + substr($_SERVER['SCRIPT_NAME'],0, strrpos($_SERVER['SCRIPT_NAME'], '/')); + } + + protected function get_user_id() { + @session_start(); + return session_id(); + } + + protected function get_user_path() { + if ($this->options['user_dirs']) { + return $this->get_user_id().'/'; + } + return ''; + } + + protected function get_upload_path($file_name = null, $version = null) { + $file_name = $file_name ? $file_name : ''; + if (empty($version)) { + $version_path = ''; + } else { + $version_dir = @$this->options['image_versions'][$version]['upload_dir']; + if ($version_dir) { + return $version_dir.$this->get_user_path().$file_name; + } + $version_path = $version.'/'; + } + return $this->options['upload_dir'].$this->get_user_path() + .$version_path.$file_name; + } + + protected function get_query_separator($url) { + return strpos($url, '?') === false ? '?' : '&'; + } + + protected function get_download_url($file_name, $version = null, $direct = false) { + if (!$direct && $this->options['download_via_php']) { + $url = $this->options['script_url'] + .$this->get_query_separator($this->options['script_url']) + .$this->get_singular_param_name() + .'='.rawurlencode($file_name); + if ($version) { + $url .= '&version='.rawurlencode($version); + } + return $url.'&download=1'; + } + if (empty($version)) { + $version_path = ''; + } else { + $version_url = @$this->options['image_versions'][$version]['upload_url']; + if ($version_url) { + return $version_url.$this->get_user_path().rawurlencode($file_name); + } + $version_path = rawurlencode($version).'/'; + } + return $this->options['upload_url'].$this->get_user_path() + .$version_path.rawurlencode($file_name); + } + + protected function set_additional_file_properties($file) { + $file->deleteUrl = $this->options['script_url'] + .$this->get_query_separator($this->options['script_url']) + .$this->get_singular_param_name() + .'='.rawurlencode($file->name); + $file->deleteType = $this->options['delete_type']; + if ($file->deleteType !== 'DELETE') { + $file->deleteUrl .= '&_method=DELETE'; + } + if ($this->options['access_control_allow_credentials']) { + $file->deleteWithCredentials = true; + } + } + + // Fix for overflowing signed 32 bit integers, + // works for sizes up to 2^32-1 bytes (4 GiB - 1): + protected function fix_integer_overflow($size) { + if ($size < 0) { + $size += 2.0 * (PHP_INT_MAX + 1); + } + return $size; + } + + protected function get_file_size($file_path, $clear_stat_cache = false) { + if ($clear_stat_cache) { + if (version_compare(PHP_VERSION, '5.3.0') >= 0) { + clearstatcache(true, $file_path); + } else { + clearstatcache(); + } + } + return $this->fix_integer_overflow(filesize($file_path)); + } + + protected function is_valid_file_object($file_name) { + $file_path = $this->get_upload_path($file_name); + if (is_file($file_path) && $file_name[0] !== '.') { + return true; + } + return false; + } + + protected function get_file_object($file_name) { + if ($this->is_valid_file_object($file_name)) { + $file = new \stdClass(); + $file->name = $file_name; + $file->size = $this->get_file_size( + $this->get_upload_path($file_name) + ); + $file->url = $this->get_download_url($file->name); + foreach ($this->options['image_versions'] as $version => $options) { + if (!empty($version)) { + if (is_file($this->get_upload_path($file_name, $version))) { + $file->{$version.'Url'} = $this->get_download_url( + $file->name, + $version + ); + } + } + } + $this->set_additional_file_properties($file); + return $file; + } + return null; + } + + protected function get_file_objects($iteration_method = 'get_file_object') { + $upload_dir = $this->get_upload_path(); + if (!is_dir($upload_dir)) { + return array(); + } + return array_values(array_filter(array_map( + array($this, $iteration_method), + scandir($upload_dir) + ))); + } + + protected function count_file_objects() { + return count($this->get_file_objects('is_valid_file_object')); + } + + protected function get_error_message($error) { + return isset($this->error_messages[$error]) ? + $this->error_messages[$error] : $error; + } + + public function get_config_bytes($val) { + $val = trim($val); + $last = strtolower($val[strlen($val)-1]); + $val = (int)$val; + switch ($last) { + case 'g': + $val *= 1024; + case 'm': + $val *= 1024; + case 'k': + $val *= 1024; + } + return $this->fix_integer_overflow($val); + } + + protected function validate($uploaded_file, $file, $error, $index) { + if ($error) { + $file->error = $this->get_error_message($error); + return false; + } + $content_length = $this->fix_integer_overflow( + (int)$this->get_server_var('CONTENT_LENGTH') + ); + $post_max_size = $this->get_config_bytes(ini_get('post_max_size')); + if ($post_max_size && ($content_length > $post_max_size)) { + $file->error = $this->get_error_message('post_max_size'); + return false; + } + if (!preg_match($this->options['accept_file_types'], $file->name)) { + $file->error = $this->get_error_message('accept_file_types'); + return false; + } + if ($uploaded_file && is_uploaded_file($uploaded_file)) { + $file_size = $this->get_file_size($uploaded_file); + } else { + $file_size = $content_length; + } + if ($this->options['max_file_size'] && ( + $file_size > $this->options['max_file_size'] || + $file->size > $this->options['max_file_size']) + ) { + $file->error = $this->get_error_message('max_file_size'); + return false; + } + if ($this->options['min_file_size'] && + $file_size < $this->options['min_file_size']) { + $file->error = $this->get_error_message('min_file_size'); + return false; + } + if (is_int($this->options['max_number_of_files']) && + ($this->count_file_objects() >= $this->options['max_number_of_files']) && + // Ignore additional chunks of existing files: + !is_file($this->get_upload_path($file->name))) { + $file->error = $this->get_error_message('max_number_of_files'); + return false; + } + $max_width = @$this->options['max_width']; + $max_height = @$this->options['max_height']; + $min_width = @$this->options['min_width']; + $min_height = @$this->options['min_height']; + if (($max_width || $max_height || $min_width || $min_height) + && preg_match($this->options['image_file_types'], $file->name)) { + list($img_width, $img_height) = $this->get_image_size($uploaded_file); + + // If we are auto rotating the image by default, do the checks on + // the correct orientation + if ( + @$this->options['image_versions']['']['auto_orient'] && + function_exists('exif_read_data') && + ($exif = @exif_read_data($uploaded_file)) && + (((int) @$exif['Orientation']) >= 5) + ) { + $tmp = $img_width; + $img_width = $img_height; + $img_height = $tmp; + unset($tmp); + } + + } + if (!empty($img_width)) { + if ($max_width && $img_width > $max_width) { + $file->error = $this->get_error_message('max_width'); + return false; + } + if ($max_height && $img_height > $max_height) { + $file->error = $this->get_error_message('max_height'); + return false; + } + if ($min_width && $img_width < $min_width) { + $file->error = $this->get_error_message('min_width'); + return false; + } + if ($min_height && $img_height < $min_height) { + $file->error = $this->get_error_message('min_height'); + return false; + } + } + return true; + } + + protected function upcount_name_callback($matches) { + $index = isset($matches[1]) ? ((int)$matches[1]) + 1 : 1; + $ext = isset($matches[2]) ? $matches[2] : ''; + return ' ('.$index.')'.$ext; + } + + protected function upcount_name($name) { + return preg_replace_callback( + '/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/', + array($this, 'upcount_name_callback'), + $name, + 1 + ); + } + + protected function get_unique_filename($file_path, $name, $size, $type, $error, + $index, $content_range) { + while(is_dir($this->get_upload_path($name))) { + $name = $this->upcount_name($name); + } + // Keep an existing filename if this is part of a chunked upload: + $uploaded_bytes = $this->fix_integer_overflow((int)$content_range[1]); + while (is_file($this->get_upload_path($name))) { + if ($uploaded_bytes === $this->get_file_size( + $this->get_upload_path($name))) { + break; + } + $name = $this->upcount_name($name); + } + return $name; + } + + protected function fix_file_extension($file_path, $name, $size, $type, $error, + $index, $content_range) { + // Add missing file extension for known image types: + if (strpos($name, '.') === false && + preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) { + $name .= '.'.$matches[1]; + } + if ($this->options['correct_image_extensions'] && + function_exists('exif_imagetype')) { + switch (@exif_imagetype($file_path)){ + case IMAGETYPE_JPEG: + $extensions = array('jpg', 'jpeg'); + break; + case IMAGETYPE_PNG: + $extensions = array('png'); + break; + case IMAGETYPE_GIF: + $extensions = array('gif'); + break; + } + // Adjust incorrect image file extensions: + if (!empty($extensions)) { + $parts = explode('.', $name); + $extIndex = count($parts) - 1; + $ext = strtolower(@$parts[$extIndex]); + if (!in_array($ext, $extensions)) { + $parts[$extIndex] = $extensions[0]; + $name = implode('.', $parts); + } + } + } + return $name; + } + + protected function trim_file_name($file_path, $name, $size, $type, $error, + $index, $content_range) { + // Remove path information and dots around the filename, to prevent uploading + // into different directories or replacing hidden system files. + // Also remove control characters and spaces (\x00..\x20) around the filename: + $name = trim($this->basename(stripslashes($name)), ".\x00..\x20"); + // Use a timestamp for empty filenames: + if (!$name) { + $name = str_replace('.', '-', microtime(true)); + } + return $name; + } + + protected function get_file_name($file_path, $name, $size, $type, $error, + $index, $content_range) { + $name = $this->trim_file_name($file_path, $name, $size, $type, $error, + $index, $content_range); + return $this->get_unique_filename( + $file_path, + $this->fix_file_extension($file_path, $name, $size, $type, $error, + $index, $content_range), + $size, + $type, + $error, + $index, + $content_range + ); + } + + protected function get_scaled_image_file_paths($file_name, $version) { + $file_path = $this->get_upload_path($file_name); + if (!empty($version)) { + $version_dir = $this->get_upload_path(null, $version); + if (!is_dir($version_dir)) { + mkdir($version_dir, $this->options['mkdir_mode'], true); + } + $new_file_path = $version_dir.'/'.$file_name; + } else { + $new_file_path = $file_path; + } + return array($file_path, $new_file_path); + } + + protected function gd_get_image_object($file_path, $func, $no_cache = false) { + if (empty($this->image_objects[$file_path]) || $no_cache) { + $this->gd_destroy_image_object($file_path); + $this->image_objects[$file_path] = $func($file_path); + } + return $this->image_objects[$file_path]; + } + + protected function gd_set_image_object($file_path, $image) { + $this->gd_destroy_image_object($file_path); + $this->image_objects[$file_path] = $image; + } + + protected function gd_destroy_image_object($file_path) { + $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ; + return $image && imagedestroy($image); + } + + protected function gd_imageflip($image, $mode) { + if (function_exists('imageflip')) { + return imageflip($image, $mode); + } + $new_width = $src_width = imagesx($image); + $new_height = $src_height = imagesy($image); + $new_img = imagecreatetruecolor($new_width, $new_height); + $src_x = 0; + $src_y = 0; + switch ($mode) { + case '1': // flip on the horizontal axis + $src_y = $new_height - 1; + $src_height = -$new_height; + break; + case '2': // flip on the vertical axis + $src_x = $new_width - 1; + $src_width = -$new_width; + break; + case '3': // flip on both axes + $src_y = $new_height - 1; + $src_height = -$new_height; + $src_x = $new_width - 1; + $src_width = -$new_width; + break; + default: + return $image; + } + imagecopyresampled( + $new_img, + $image, + 0, + 0, + $src_x, + $src_y, + $new_width, + $new_height, + $src_width, + $src_height + ); + return $new_img; + } + + protected function gd_orient_image($file_path, $src_img) { + if (!function_exists('exif_read_data')) { + return false; + } + $exif = @exif_read_data($file_path); + if ($exif === false) { + return false; + } + $orientation = (int)@$exif['Orientation']; + if ($orientation < 2 || $orientation > 8) { + return false; + } + switch ($orientation) { + case 2: + $new_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2 + ); + break; + case 3: + $new_img = imagerotate($src_img, 180, 0); + break; + case 4: + $new_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1 + ); + break; + case 5: + $tmp_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1 + ); + $new_img = imagerotate($tmp_img, 270, 0); + imagedestroy($tmp_img); + break; + case 6: + $new_img = imagerotate($src_img, 270, 0); + break; + case 7: + $tmp_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2 + ); + $new_img = imagerotate($tmp_img, 270, 0); + imagedestroy($tmp_img); + break; + case 8: + $new_img = imagerotate($src_img, 90, 0); + break; + default: + return false; + } + $this->gd_set_image_object($file_path, $new_img); + return true; + } + + protected function gd_create_scaled_image($file_name, $version, $options) { + if (!function_exists('imagecreatetruecolor')) { + error_log('Function not found: imagecreatetruecolor'); + return false; + } + list($file_path, $new_file_path) = + $this->get_scaled_image_file_paths($file_name, $version); + $type = strtolower(substr(strrchr($file_name, '.'), 1)); + switch ($type) { + case 'jpg': + case 'jpeg': + $src_func = 'imagecreatefromjpeg'; + $write_func = 'imagejpeg'; + $image_quality = isset($options['jpeg_quality']) ? + $options['jpeg_quality'] : 75; + break; + case 'gif': + $src_func = 'imagecreatefromgif'; + $write_func = 'imagegif'; + $image_quality = null; + break; + case 'png': + $src_func = 'imagecreatefrompng'; + $write_func = 'imagepng'; + $image_quality = isset($options['png_quality']) ? + $options['png_quality'] : 9; + break; + default: + return false; + } + $src_img = $this->gd_get_image_object( + $file_path, + $src_func, + !empty($options['no_cache']) + ); + $image_oriented = false; + if (!empty($options['auto_orient']) && $this->gd_orient_image( + $file_path, + $src_img + )) { + $image_oriented = true; + $src_img = $this->gd_get_image_object( + $file_path, + $src_func + ); + } + $max_width = $img_width = imagesx($src_img); + $max_height = $img_height = imagesy($src_img); + if (!empty($options['max_width'])) { + $max_width = $options['max_width']; + } + if (!empty($options['max_height'])) { + $max_height = $options['max_height']; + } + $scale = min( + $max_width / $img_width, + $max_height / $img_height + ); + if ($scale >= 1) { + if ($image_oriented) { + return $write_func($src_img, $new_file_path, $image_quality); + } + if ($file_path !== $new_file_path) { + return copy($file_path, $new_file_path); + } + return true; + } + if (empty($options['crop'])) { + $new_width = $img_width * $scale; + $new_height = $img_height * $scale; + $dst_x = 0; + $dst_y = 0; + $new_img = imagecreatetruecolor($new_width, $new_height); + } else { + if (($img_width / $img_height) >= ($max_width / $max_height)) { + $new_width = $img_width / ($img_height / $max_height); + $new_height = $max_height; + } else { + $new_width = $max_width; + $new_height = $img_height / ($img_width / $max_width); + } + $dst_x = 0 - ($new_width - $max_width) / 2; + $dst_y = 0 - ($new_height - $max_height) / 2; + $new_img = imagecreatetruecolor($max_width, $max_height); + } + // Handle transparency in GIF and PNG images: + switch ($type) { + case 'gif': + case 'png': + imagecolortransparent($new_img, imagecolorallocate($new_img, 0, 0, 0)); + case 'png': + imagealphablending($new_img, false); + imagesavealpha($new_img, true); + break; + } + $success = imagecopyresampled( + $new_img, + $src_img, + $dst_x, + $dst_y, + 0, + 0, + $new_width, + $new_height, + $img_width, + $img_height + ) && $write_func($new_img, $new_file_path, $image_quality); + $this->gd_set_image_object($file_path, $new_img); + return $success; + } + + protected function imagick_get_image_object($file_path, $no_cache = false) { + if (empty($this->image_objects[$file_path]) || $no_cache) { + $this->imagick_destroy_image_object($file_path); + $image = new \Imagick(); + if (!empty($this->options['imagick_resource_limits'])) { + foreach ($this->options['imagick_resource_limits'] as $type => $limit) { + $image->setResourceLimit($type, $limit); + } + } + $image->readImage($file_path); + $this->image_objects[$file_path] = $image; + } + return $this->image_objects[$file_path]; + } + + protected function imagick_set_image_object($file_path, $image) { + $this->imagick_destroy_image_object($file_path); + $this->image_objects[$file_path] = $image; + } + + protected function imagick_destroy_image_object($file_path) { + $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ; + return $image && $image->destroy(); + } + + protected function imagick_orient_image($image) { + $orientation = $image->getImageOrientation(); + $background = new \ImagickPixel('none'); + switch ($orientation) { + case \imagick::ORIENTATION_TOPRIGHT: // 2 + $image->flopImage(); // horizontal flop around y-axis + break; + case \imagick::ORIENTATION_BOTTOMRIGHT: // 3 + $image->rotateImage($background, 180); + break; + case \imagick::ORIENTATION_BOTTOMLEFT: // 4 + $image->flipImage(); // vertical flip around x-axis + break; + case \imagick::ORIENTATION_LEFTTOP: // 5 + $image->flopImage(); // horizontal flop around y-axis + $image->rotateImage($background, 270); + break; + case \imagick::ORIENTATION_RIGHTTOP: // 6 + $image->rotateImage($background, 90); + break; + case \imagick::ORIENTATION_RIGHTBOTTOM: // 7 + $image->flipImage(); // vertical flip around x-axis + $image->rotateImage($background, 270); + break; + case \imagick::ORIENTATION_LEFTBOTTOM: // 8 + $image->rotateImage($background, 270); + break; + default: + return false; + } + $image->setImageOrientation(\imagick::ORIENTATION_TOPLEFT); // 1 + return true; + } + + protected function imagick_create_scaled_image($file_name, $version, $options) { + list($file_path, $new_file_path) = + $this->get_scaled_image_file_paths($file_name, $version); + $image = $this->imagick_get_image_object( + $file_path, + !empty($options['crop']) || !empty($options['no_cache']) + ); + if ($image->getImageFormat() === 'GIF') { + // Handle animated GIFs: + $images = $image->coalesceImages(); + foreach ($images as $frame) { + $image = $frame; + $this->imagick_set_image_object($file_name, $image); + break; + } + } + $image_oriented = false; + if (!empty($options['auto_orient'])) { + $image_oriented = $this->imagick_orient_image($image); + } + + $image_resize = false; + $new_width = $max_width = $img_width = $image->getImageWidth(); + $new_height = $max_height = $img_height = $image->getImageHeight(); + + // use isset(). User might be setting max_width = 0 (auto in regular resizing). Value 0 would be considered empty when you use empty() + if (isset($options['max_width'])) { + $image_resize = true; + $new_width = $max_width = $options['max_width']; + } + if (isset($options['max_height'])) { + $image_resize = true; + $new_height = $max_height = $options['max_height']; + } + + $image_strip = (isset($options['strip']) ? $options['strip'] : false); + + if ( !$image_oriented && ($max_width >= $img_width) && ($max_height >= $img_height) && !$image_strip && empty($options["jpeg_quality"]) ) { + if ($file_path !== $new_file_path) { + return copy($file_path, $new_file_path); + } + return true; + } + $crop = (isset($options['crop']) ? $options['crop'] : false); + + if ($crop) { + $x = 0; + $y = 0; + if (($img_width / $img_height) >= ($max_width / $max_height)) { + $new_width = 0; // Enables proportional scaling based on max_height + $x = ($img_width / ($img_height / $max_height) - $max_width) / 2; + } else { + $new_height = 0; // Enables proportional scaling based on max_width + $y = ($img_height / ($img_width / $max_width) - $max_height) / 2; + } + } + $success = $image->resizeImage( + $new_width, + $new_height, + isset($options['filter']) ? $options['filter'] : \imagick::FILTER_LANCZOS, + isset($options['blur']) ? $options['blur'] : 1, + $new_width && $new_height // fit image into constraints if not to be cropped + ); + if ($success && $crop) { + $success = $image->cropImage( + $max_width, + $max_height, + $x, + $y + ); + if ($success) { + $success = $image->setImagePage($max_width, $max_height, 0, 0); + } + } + $type = strtolower(substr(strrchr($file_name, '.'), 1)); + switch ($type) { + case 'jpg': + case 'jpeg': + if (!empty($options['jpeg_quality'])) { + $image->setImageCompression(\imagick::COMPRESSION_JPEG); + $image->setImageCompressionQuality($options['jpeg_quality']); + } + break; + } + if ( $image_strip ) { + $image->stripImage(); + } + return $success && $image->writeImage($new_file_path); + } + + protected function imagemagick_create_scaled_image($file_name, $version, $options) { + list($file_path, $new_file_path) = + $this->get_scaled_image_file_paths($file_name, $version); + $resize = @$options['max_width'] + .(empty($options['max_height']) ? '' : 'X'.$options['max_height']); + if (!$resize && empty($options['auto_orient'])) { + if ($file_path !== $new_file_path) { + return copy($file_path, $new_file_path); + } + return true; + } + $cmd = $this->options['convert_bin']; + if (!empty($this->options['convert_params'])) { + $cmd .= ' '.$this->options['convert_params']; + } + $cmd .= ' '.escapeshellarg($file_path); + if (!empty($options['auto_orient'])) { + $cmd .= ' -auto-orient'; + } + if ($resize) { + // Handle animated GIFs: + $cmd .= ' -coalesce'; + if (empty($options['crop'])) { + $cmd .= ' -resize '.escapeshellarg($resize.'>'); + } else { + $cmd .= ' -resize '.escapeshellarg($resize.'^'); + $cmd .= ' -gravity center'; + $cmd .= ' -crop '.escapeshellarg($resize.'+0+0'); + } + // Make sure the page dimensions are correct (fixes offsets of animated GIFs): + $cmd .= ' +repage'; + } + if (!empty($options['convert_params'])) { + $cmd .= ' '.$options['convert_params']; + } + $cmd .= ' '.escapeshellarg($new_file_path); + exec($cmd, $output, $error); + if ($error) { + error_log(implode('\n', $output)); + return false; + } + return true; + } + + protected function get_image_size($file_path) { + if ($this->options['image_library']) { + if (extension_loaded('imagick')) { + $image = new \Imagick(); + try { + if (@$image->pingImage($file_path)) { + $dimensions = array($image->getImageWidth(), $image->getImageHeight()); + $image->destroy(); + return $dimensions; + } + return false; + } catch (\Exception $e) { + error_log($e->getMessage()); + } + } + if ($this->options['image_library'] === 2) { + $cmd = $this->options['identify_bin']; + $cmd .= ' -ping '.escapeshellarg($file_path); + exec($cmd, $output, $error); + if (!$error && !empty($output)) { + // image.jpg JPEG 1920x1080 1920x1080+0+0 8-bit sRGB 465KB 0.000u 0:00.000 + $infos = preg_split('/\s+/', substr($output[0], strlen($file_path))); + $dimensions = preg_split('/x/', $infos[2]); + return $dimensions; + } + return false; + } + } + if (!function_exists('getimagesize')) { + error_log('Function not found: getimagesize'); + return false; + } + return @getimagesize($file_path); + } + + protected function create_scaled_image($file_name, $version, $options) { + if ($this->options['image_library'] === 2) { + return $this->imagemagick_create_scaled_image($file_name, $version, $options); + } + if ($this->options['image_library'] && extension_loaded('imagick')) { + return $this->imagick_create_scaled_image($file_name, $version, $options); + } + return $this->gd_create_scaled_image($file_name, $version, $options); + } + + protected function destroy_image_object($file_path) { + if ($this->options['image_library'] && extension_loaded('imagick')) { + return $this->imagick_destroy_image_object($file_path); + } + } + + protected function is_valid_image_file($file_path) { + if (!preg_match($this->options['image_file_types'], $file_path)) { + return false; + } + if (function_exists('exif_imagetype')) { + return @exif_imagetype($file_path); + } + $image_info = $this->get_image_size($file_path); + return $image_info && $image_info[0] && $image_info[1]; + } + + protected function handle_image_file($file_path, $file) { + $failed_versions = array(); + foreach ($this->options['image_versions'] as $version => $options) { + if ($this->create_scaled_image($file->name, $version, $options)) { + if (!empty($version)) { + $file->{$version.'Url'} = $this->get_download_url( + $file->name, + $version + ); + } else { + $file->size = $this->get_file_size($file_path, true); + } + } else { + $failed_versions[] = $version ? $version : 'original'; + } + } + if (count($failed_versions)) { + $file->error = $this->get_error_message('image_resize') + .' ('.implode($failed_versions, ', ').')'; + } + // Free memory: + $this->destroy_image_object($file_path); + } + + protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, + $index = null, $content_range = null) { + $file = new \stdClass(); + $file->name = $this->get_file_name($uploaded_file, $name, $size, $type, $error, + $index, $content_range); + $file->size = $this->fix_integer_overflow((int)$size); + $file->type = $type; + if ($this->validate($uploaded_file, $file, $error, $index)) { + $this->handle_form_data($file, $index); + $upload_dir = $this->get_upload_path(); + if (!is_dir($upload_dir)) { + mkdir($upload_dir, $this->options['mkdir_mode'], true); + } + $file_path = $this->get_upload_path($file->name); + $append_file = $content_range && is_file($file_path) && + $file->size > $this->get_file_size($file_path); + if ($uploaded_file && is_uploaded_file($uploaded_file)) { + // multipart/formdata uploads (POST method uploads) + if ($append_file) { + file_put_contents( + $file_path, + fopen($uploaded_file, 'r'), + FILE_APPEND + ); + } else { + move_uploaded_file($uploaded_file, $file_path); + } + } else { + // Non-multipart uploads (PUT method support) + file_put_contents( + $file_path, + fopen($this->options['input_stream'], 'r'), + $append_file ? FILE_APPEND : 0 + ); + } + $file_size = $this->get_file_size($file_path, $append_file); + if ($file_size === $file->size) { + $file->url = $this->get_download_url($file->name); + if ($this->is_valid_image_file($file_path)) { + $this->handle_image_file($file_path, $file); + } + } else { + $file->size = $file_size; + if (!$content_range && $this->options['discard_aborted_uploads']) { + unlink($file_path); + $file->error = $this->get_error_message('abort'); + } + } + $this->set_additional_file_properties($file); + } + return $file; + } + + protected function readfile($file_path) { + $file_size = $this->get_file_size($file_path); + $chunk_size = $this->options['readfile_chunk_size']; + if ($chunk_size && $file_size > $chunk_size) { + $handle = fopen($file_path, 'rb'); + while (!feof($handle)) { + echo fread($handle, $chunk_size); + @ob_flush(); + @flush(); + } + fclose($handle); + return $file_size; + } + return readfile($file_path); + } + + protected function body($str) { + echo $str; + } + + protected function header($str) { + header($str); + } + + protected function get_upload_data($id) { + return @$_FILES[$id]; + } + + protected function get_post_param($id) { + return @$_POST[$id]; + } + + protected function get_query_param($id) { + return @$_GET[$id]; + } + + protected function get_server_var($id) { + return @$_SERVER[$id]; + } + + protected function handle_form_data($file, $index) { + // Handle form data, e.g. $_POST['description'][$index] + } + + protected function get_version_param() { + return $this->basename(stripslashes($this->get_query_param('version'))); + } + + protected function get_singular_param_name() { + return substr($this->options['param_name'], 0, -1); + } + + protected function get_file_name_param() { + $name = $this->get_singular_param_name(); + return $this->basename(stripslashes($this->get_query_param($name))); + } + + protected function get_file_names_params() { + $params = $this->get_query_param($this->options['param_name']); + if (!$params) { + return null; + } + foreach ($params as $key => $value) { + $params[$key] = $this->basename(stripslashes($value)); + } + return $params; + } + + protected function get_file_type($file_path) { + switch (strtolower(pathinfo($file_path, PATHINFO_EXTENSION))) { + case 'jpeg': + case 'jpg': + return 'image/jpeg'; + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + default: + return ''; + } + } + + protected function download() { + switch ($this->options['download_via_php']) { + case 1: + $redirect_header = null; + break; + case 2: + $redirect_header = 'X-Sendfile'; + break; + case 3: + $redirect_header = 'X-Accel-Redirect'; + break; + default: + return $this->header('HTTP/1.1 403 Forbidden'); + } + $file_name = $this->get_file_name_param(); + if (!$this->is_valid_file_object($file_name)) { + return $this->header('HTTP/1.1 404 Not Found'); + } + if ($redirect_header) { + return $this->header( + $redirect_header.': '.$this->get_download_url( + $file_name, + $this->get_version_param(), + true + ) + ); + } + $file_path = $this->get_upload_path($file_name, $this->get_version_param()); + // Prevent browsers from MIME-sniffing the content-type: + $this->header('X-Content-Type-Options: nosniff'); + if (!preg_match($this->options['inline_file_types'], $file_name)) { + $this->header('Content-Type: application/octet-stream'); + $this->header('Content-Disposition: attachment; filename="'.$file_name.'"'); + } else { + $this->header('Content-Type: '.$this->get_file_type($file_path)); + $this->header('Content-Disposition: inline; filename="'.$file_name.'"'); + } + $this->header('Content-Length: '.$this->get_file_size($file_path)); + $this->header('Last-Modified: '.gmdate('D, d M Y H:i:s T', filemtime($file_path))); + $this->readfile($file_path); + } + + protected function send_content_type_header() { + $this->header('Vary: Accept'); + if (strpos($this->get_server_var('HTTP_ACCEPT'), 'application/json') !== false) { + $this->header('Content-type: application/json'); + } else { + $this->header('Content-type: text/plain'); + } + } + + protected function send_access_control_headers() { + $this->header('Access-Control-Allow-Origin: '.$this->options['access_control_allow_origin']); + $this->header('Access-Control-Allow-Credentials: ' + .($this->options['access_control_allow_credentials'] ? 'true' : 'false')); + $this->header('Access-Control-Allow-Methods: ' + .implode(', ', $this->options['access_control_allow_methods'])); + $this->header('Access-Control-Allow-Headers: ' + .implode(', ', $this->options['access_control_allow_headers'])); + } + + public function generate_response($content, $print_response = true) { + $this->response = $content; + if ($print_response) { + $json = json_encode($content); + $redirect = stripslashes($this->get_post_param('redirect')); + if ($redirect && preg_match($this->options['redirect_allow_target'], $redirect)) { + $this->header('Location: '.sprintf($redirect, rawurlencode($json))); + return; + } + $this->head(); + if ($this->get_server_var('HTTP_CONTENT_RANGE')) { + $files = isset($content[$this->options['param_name']]) ? + $content[$this->options['param_name']] : null; + if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) { + $this->header('Range: 0-'.( + $this->fix_integer_overflow((int)$files[0]->size) - 1 + )); + } + } + $this->body($json); + } + return $content; + } + + public function get_response () { + return $this->response; + } + + public function head() { + $this->header('Pragma: no-cache'); + $this->header('Cache-Control: no-store, no-cache, must-revalidate'); + $this->header('Content-Disposition: inline; filename="files.json"'); + // Prevent Internet Explorer from MIME-sniffing the content-type: + $this->header('X-Content-Type-Options: nosniff'); + if ($this->options['access_control_allow_origin']) { + $this->send_access_control_headers(); + } + $this->send_content_type_header(); + } + + public function get($print_response = true) { + if ($print_response && $this->get_query_param('download')) { + return $this->download(); + } + $file_name = $this->get_file_name_param(); + if ($file_name) { + $response = array( + $this->get_singular_param_name() => $this->get_file_object($file_name) + ); + } else { + $response = array( + $this->options['param_name'] => $this->get_file_objects() + ); + } + return $this->generate_response($response, $print_response); + } + + public function post($print_response = true) { + if ($this->get_query_param('_method') === 'DELETE') { + return $this->delete($print_response); + } + $upload = $this->get_upload_data($this->options['param_name']); + // Parse the Content-Disposition header, if available: + $content_disposition_header = $this->get_server_var('HTTP_CONTENT_DISPOSITION'); + $file_name = $content_disposition_header ? + rawurldecode(preg_replace( + '/(^[^"]+")|("$)/', + '', + $content_disposition_header + )) : null; + // Parse the Content-Range header, which has the following form: + // Content-Range: bytes 0-524287/2000000 + $content_range_header = $this->get_server_var('HTTP_CONTENT_RANGE'); + $content_range = $content_range_header ? + preg_split('/[^0-9]+/', $content_range_header) : null; + $size = $content_range ? $content_range[3] : null; + $files = array(); + if ($upload) { + if (is_array($upload['tmp_name'])) { + // param_name is an array identifier like "files[]", + // $upload is a multi-dimensional array: + foreach ($upload['tmp_name'] as $index => $value) { + $files[] = $this->handle_file_upload( + $upload['tmp_name'][$index], + $file_name ? $file_name : $upload['name'][$index], + $size ? $size : $upload['size'][$index], + $upload['type'][$index], + $upload['error'][$index], + $index, + $content_range + ); + } + } else { + // param_name is a single object identifier like "file", + // $upload is a one-dimensional array: + $files[] = $this->handle_file_upload( + isset($upload['tmp_name']) ? $upload['tmp_name'] : null, + $file_name ? $file_name : (isset($upload['name']) ? + $upload['name'] : null), + $size ? $size : (isset($upload['size']) ? + $upload['size'] : $this->get_server_var('CONTENT_LENGTH')), + isset($upload['type']) ? + $upload['type'] : $this->get_server_var('CONTENT_TYPE'), + isset($upload['error']) ? $upload['error'] : null, + null, + $content_range + ); + } + } + $response = array($this->options['param_name'] => $files); + return $this->generate_response($response, $print_response); + } + + public function delete($print_response = true) { + $file_names = $this->get_file_names_params(); + if (empty($file_names)) { + $file_names = array($this->get_file_name_param()); + } + $response = array(); + foreach ($file_names as $file_name) { + $file_path = $this->get_upload_path($file_name); + $success = is_file($file_path) && $file_name[0] !== '.' && unlink($file_path); + if ($success) { + foreach ($this->options['image_versions'] as $version => $options) { + if (!empty($version)) { + $file = $this->get_upload_path($file_name, $version); + if (is_file($file)) { + unlink($file); + } + } + } + } + $response[$file_name] = $success; + } + return $this->generate_response($response, $print_response); + } + + protected function basename($filepath, $suffix = null) { + $splited = preg_split('/\//', rtrim ($filepath, '/ ')); + return substr(basename('X'.$splited[count($splited)-1], $suffix), 1); + } +} diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/docker-compose.yml b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/docker-compose.yml new file mode 100644 index 0000000..74eabf7 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/docker-compose.yml @@ -0,0 +1,9 @@ +version: '2.3' +services: + apache: + build: ./ + network_mode: bridge + ports: + - "80:80" + volumes: + - "../../:/var/www/html" diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/files/.gitignore b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/files/.gitignore new file mode 100644 index 0000000..e24a60f --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/files/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!.htaccess diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/files/.htaccess b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/files/.htaccess new file mode 100644 index 0000000..6f454af --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/files/.htaccess @@ -0,0 +1,26 @@ +# To enable the Headers module, execute the following command and reload Apache: +# sudo a2enmod headers + +# The following directives prevent the execution of script files +# in the context of the website. +# They also force the content-type application/octet-stream and +# force browsers to display a download dialog for non-image files. +SetHandler default-handler +ForceType application/octet-stream +Header set Content-Disposition attachment + +# The following unsets the forced type and Content-Disposition headers +# for known image files: + + ForceType none + Header unset Content-Disposition + + +# The following directive prevents browsers from MIME-sniffing the content-type. +# This is an important complement to the ForceType directive above: +Header set X-Content-Type-Options nosniff + +# Uncomment the following lines to prevent unauthorized download of files: +#AuthName "Authorization required" +#AuthType Basic +#require valid-user diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/index.php b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/index.php new file mode 100644 index 0000000..6caabb7 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/server/php/index.php @@ -0,0 +1,15 @@ + + + + + + + +jQuery File Upload Plugin Test + + + + +

jQuery File Upload Plugin Test

+

+
+

+
    +
    + +
    + +
    +
    + + + + Add files... + + + + + + + + +
    + +
    + +
    +
    +
    + +
     
    +
    +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/files/static/files/jQuery-File-Upload-9.21.0/test/test.js b/apps/files/static/files/jQuery-File-Upload-9.21.0/test/test.js new file mode 100644 index 0000000..4521275 --- /dev/null +++ b/apps/files/static/files/jQuery-File-Upload-9.21.0/test/test.js @@ -0,0 +1,1292 @@ +/* + * jQuery File Upload Plugin Test + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global $, QUnit, window, document, expect, module, test, asyncTest, start, ok, strictEqual, notStrictEqual */ + +$(function () { + // jshint nomen:false + 'use strict'; + + QUnit.done = function () { + // Delete all uploaded files: + var url = $('#fileupload').prop('action'); + $.getJSON(url, function (result) { + $.each(result.files, function (index, file) { + $.ajax({ + url: url + '?file=' + encodeURIComponent(file.name), + type: 'DELETE' + }); + }); + }); + }; + + var lifecycle = { + setup: function () { + // Set the .fileupload method to the basic widget method: + $.widget('blueimp.fileupload', window.testBasicWidget, {}); + }, + teardown: function () { + // Remove all remaining event listeners: + $(document).unbind(); + } + }, + lifecycleUI = { + setup: function () { + // Set the .fileupload method to the UI widget method: + $.widget('blueimp.fileupload', window.testUIWidget, {}); + }, + teardown: function () { + // Remove all remaining event listeners: + $(document).unbind(); + } + }; + + module('Initialization', lifecycle); + + test('Widget initialization', function () { + var fu = $('#fileupload').fileupload(); + ok(fu.data('blueimp-fileupload') || fu.data('fileupload')); + }); + + test('Data attribute options', function () { + $('#fileupload').attr('data-url', 'http://example.org'); + $('#fileupload').fileupload(); + strictEqual( + $('#fileupload').fileupload('option', 'url'), + 'http://example.org' + ); + }); + + test('File input initialization', function () { + var fu = $('#fileupload').fileupload(); + ok( + fu.fileupload('option', 'fileInput').length, + 'File input field inside of the widget' + ); + ok( + fu.fileupload('option', 'fileInput').length, + 'Widget element as file input field' + ); + }); + + test('Drop zone initialization', function () { + ok($('#fileupload').fileupload() + .fileupload('option', 'dropZone').length); + }); + + test('Paste zone initialization', function () { + ok($('#fileupload').fileupload({pasteZone: document}) + .fileupload('option', 'pasteZone').length); + }); + + test('Event listeners initialization', function () { + expect( + $.support.xhrFormDataFileUpload ? 4 : 1 + ); + var eo = { + originalEvent: { + dataTransfer: {files: [{}], types: ['Files']}, + clipboardData: {items: [{}]} + } + }, + fu = $('#fileupload').fileupload({ + pasteZone: document, + dragover: function () { + ok(true, 'Triggers dragover callback'); + return false; + }, + drop: function () { + ok(true, 'Triggers drop callback'); + return false; + }, + paste: function () { + ok(true, 'Triggers paste callback'); + return false; + }, + change: function () { + ok(true, 'Triggers change callback'); + return false; + } + }), + fileInput = fu.fileupload('option', 'fileInput'), + dropZone = fu.fileupload('option', 'dropZone'), + pasteZone = fu.fileupload('option', 'pasteZone'); + fileInput.trigger($.Event('change', eo)); + dropZone.trigger($.Event('dragover', eo)); + dropZone.trigger($.Event('drop', eo)); + pasteZone.trigger($.Event('paste', eo)); + }); + + module('API', lifecycle); + + test('destroy', function () { + expect(4); + var eo = { + originalEvent: { + dataTransfer: {files: [{}], types: ['Files']}, + clipboardData: {items: [{}]} + } + }, + options = { + pasteZone: document, + dragover: function () { + ok(true, 'Triggers dragover callback'); + return false; + }, + drop: function () { + ok(true, 'Triggers drop callback'); + return false; + }, + paste: function () { + ok(true, 'Triggers paste callback'); + return false; + }, + change: function () { + ok(true, 'Triggers change callback'); + return false; + } + }, + fu = $('#fileupload').fileupload(options), + fileInput = fu.fileupload('option', 'fileInput'), + dropZone = fu.fileupload('option', 'dropZone'), + pasteZone = fu.fileupload('option', 'pasteZone'); + dropZone.bind('dragover', options.dragover); + dropZone.bind('drop', options.drop); + pasteZone.bind('paste', options.paste); + fileInput.bind('change', options.change); + fu.fileupload('destroy'); + fileInput.trigger($.Event('change', eo)); + dropZone.trigger($.Event('dragover', eo)); + dropZone.trigger($.Event('drop', eo)); + pasteZone.trigger($.Event('paste', eo)); + }); + + test('disable/enable', function () { + expect( + $.support.xhrFormDataFileUpload ? 4 : 1 + ); + var eo = { + originalEvent: { + dataTransfer: {files: [{}], types: ['Files']}, + clipboardData: {items: [{}]} + } + }, + fu = $('#fileupload').fileupload({ + pasteZone: document, + dragover: function () { + ok(true, 'Triggers dragover callback'); + return false; + }, + drop: function () { + ok(true, 'Triggers drop callback'); + return false; + }, + paste: function () { + ok(true, 'Triggers paste callback'); + return false; + }, + change: function () { + ok(true, 'Triggers change callback'); + return false; + } + }), + fileInput = fu.fileupload('option', 'fileInput'), + dropZone = fu.fileupload('option', 'dropZone'), + pasteZone = fu.fileupload('option', 'pasteZone'); + fu.fileupload('disable'); + fileInput.trigger($.Event('change', eo)); + dropZone.trigger($.Event('dragover', eo)); + dropZone.trigger($.Event('drop', eo)); + pasteZone.trigger($.Event('paste', eo)); + fu.fileupload('enable'); + fileInput.trigger($.Event('change', eo)); + dropZone.trigger($.Event('dragover', eo)); + dropZone.trigger($.Event('drop', eo)); + pasteZone.trigger($.Event('paste', eo)); + }); + + test('option', function () { + expect( + $.support.xhrFormDataFileUpload ? 10 : 7 + ); + var eo = { + originalEvent: { + dataTransfer: {files: [{}], types: ['Files']}, + clipboardData: {items: [{}]} + } + }, + fu = $('#fileupload').fileupload({ + pasteZone: document, + dragover: function () { + ok(true, 'Triggers dragover callback'); + return false; + }, + drop: function () { + ok(true, 'Triggers drop callback'); + return false; + }, + paste: function () { + ok(true, 'Triggers paste callback'); + return false; + }, + change: function () { + ok(true, 'Triggers change callback'); + return false; + } + }), + fileInput = fu.fileupload('option', 'fileInput'), + dropZone = fu.fileupload('option', 'dropZone'), + pasteZone = fu.fileupload('option', 'pasteZone'); + fu.fileupload('option', 'fileInput', null); + fu.fileupload('option', 'dropZone', null); + fu.fileupload('option', 'pasteZone', null); + fileInput.trigger($.Event('change', eo)); + dropZone.trigger($.Event('dragover', eo)); + dropZone.trigger($.Event('drop', eo)); + pasteZone.trigger($.Event('paste', eo)); + fu.fileupload('option', 'dropZone', 'body'); + strictEqual( + fu.fileupload('option', 'dropZone')[0], + document.body, + 'Allow a query string as parameter for the dropZone option' + ); + fu.fileupload('option', 'dropZone', document); + strictEqual( + fu.fileupload('option', 'dropZone')[0], + document, + 'Allow a document element as parameter for the dropZone option' + ); + fu.fileupload('option', 'pasteZone', 'body'); + strictEqual( + fu.fileupload('option', 'pasteZone')[0], + document.body, + 'Allow a query string as parameter for the pasteZone option' + ); + fu.fileupload('option', 'pasteZone', document); + strictEqual( + fu.fileupload('option', 'pasteZone')[0], + document, + 'Allow a document element as parameter for the pasteZone option' + ); + fu.fileupload('option', 'fileInput', ':file'); + strictEqual( + fu.fileupload('option', 'fileInput')[0], + $(':file')[0], + 'Allow a query string as parameter for the fileInput option' + ); + fu.fileupload('option', 'fileInput', $(':file')[0]); + strictEqual( + fu.fileupload('option', 'fileInput')[0], + $(':file')[0], + 'Allow a document element as parameter for the fileInput option' + ); + fu.fileupload('option', 'fileInput', fileInput); + fu.fileupload('option', 'dropZone', dropZone); + fu.fileupload('option', 'pasteZone', pasteZone); + fileInput.trigger($.Event('change', eo)); + dropZone.trigger($.Event('dragover', eo)); + dropZone.trigger($.Event('drop', eo)); + pasteZone.trigger($.Event('paste', eo)); + }); + + asyncTest('add', function () { + expect(2); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + add: function (e, data) { + strictEqual( + data.files[0].name, + param.files[0].name, + 'Triggers add callback' + ); + } + }).fileupload('add', param).fileupload( + 'option', + 'add', + function (e, data) { + data.submit().complete(function () { + ok(true, 'data.submit() Returns a jqXHR object'); + start(); + }); + } + ).fileupload('add', param); + }); + + asyncTest('send', function () { + expect(3); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + send: function (e, data) { + strictEqual( + data.files[0].name, + 'test', + 'Triggers send callback' + ); + } + }).fileupload('send', param).fail(function () { + ok(true, 'Allows to abort the request'); + }).complete(function () { + ok(true, 'Returns a jqXHR object'); + start(); + }).abort(); + }); + + module('Callbacks', lifecycle); + + asyncTest('add', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + add: function () { + ok(true, 'Triggers add callback'); + start(); + } + }).fileupload('add', param); + }); + + asyncTest('submit', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + submit: function () { + ok(true, 'Triggers submit callback'); + start(); + return false; + } + }).fileupload('add', param); + }); + + asyncTest('send', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + send: function () { + ok(true, 'Triggers send callback'); + start(); + return false; + } + }).fileupload('send', param); + }); + + asyncTest('done', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + done: function () { + ok(true, 'Triggers done callback'); + start(); + } + }).fileupload('send', param); + }); + + asyncTest('fail', function () { + expect(1); + var param = {files: [{name: 'test'}]}, + fu = $('#fileupload').fileupload({ + url: '404', + fail: function () { + ok(true, 'Triggers fail callback'); + start(); + } + }); + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + fu.fileupload('send', param); + }); + + asyncTest('always', function () { + expect(2); + var param = {files: [{name: 'test'}]}, + counter = 0, + fu = $('#fileupload').fileupload({ + always: function () { + ok(true, 'Triggers always callback'); + if (counter === 1) { + start(); + } else { + counter += 1; + } + } + }); + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + fu.fileupload('add', param).fileupload( + 'option', + 'url', + '404' + ).fileupload('add', param); + }); + + asyncTest('progress', function () { + expect(1); + var param = {files: [{name: 'test'}]}, + counter = 0; + $('#fileupload').fileupload({ + forceIframeTransport: true, + progress: function () { + ok(true, 'Triggers progress callback'); + if (counter === 0) { + start(); + } else { + counter += 1; + } + } + }).fileupload('send', param); + }); + + asyncTest('progressall', function () { + expect(1); + var param = {files: [{name: 'test'}]}, + counter = 0; + $('#fileupload').fileupload({ + forceIframeTransport: true, + progressall: function () { + ok(true, 'Triggers progressall callback'); + if (counter === 0) { + start(); + } else { + counter += 1; + } + } + }).fileupload('send', param); + }); + + asyncTest('start', function () { + expect(1); + var param = {files: [{name: '1'}, {name: '2'}]}, + active = 0; + $('#fileupload').fileupload({ + send: function () { + active += 1; + }, + start: function () { + ok(!active, 'Triggers start callback before uploads'); + start(); + } + }).fileupload('send', param); + }); + + asyncTest('stop', function () { + expect(1); + var param = {files: [{name: '1'}, {name: '2'}]}, + active = 0; + $('#fileupload').fileupload({ + send: function () { + active += 1; + }, + always: function () { + active -= 1; + }, + stop: function () { + ok(!active, 'Triggers stop callback after uploads'); + start(); + } + }).fileupload('send', param); + }); + + test('change', function () { + var fu = $('#fileupload').fileupload(), + fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'), + fileInput = fu.fileupload('option', 'fileInput'); + expect(2); + fu.fileupload({ + change: function (e, data) { + ok(true, 'Triggers change callback'); + strictEqual( + data.files.length, + 0, + 'Returns empty files list' + ); + }, + add: $.noop + }); + fuo._onChange({ + data: {fileupload: fuo}, + target: fileInput[0] + }); + }); + + test('paste', function () { + var fu = $('#fileupload').fileupload(), + fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'); + expect(1); + fu.fileupload({ + paste: function () { + ok(true, 'Triggers paste callback'); + }, + add: $.noop + }); + fuo._onPaste({ + data: {fileupload: fuo}, + originalEvent: { + dataTransfer: {files: [{}]}, + clipboardData: {items: [{}]} + }, + preventDefault: $.noop + }); + }); + + test('drop', function () { + var fu = $('#fileupload').fileupload(), + fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'); + expect(1); + fu.fileupload({ + drop: function () { + ok(true, 'Triggers drop callback'); + }, + add: $.noop + }); + fuo._onDrop({ + data: {fileupload: fuo}, + originalEvent: { + dataTransfer: {files: [{}]}, + clipboardData: {items: [{}]} + }, + preventDefault: $.noop + }); + }); + + test('dragover', function () { + var fu = $('#fileupload').fileupload(), + fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'); + expect(1); + fu.fileupload({ + dragover: function () { + ok(true, 'Triggers dragover callback'); + }, + add: $.noop + }); + fuo._onDragOver({ + data: {fileupload: fuo}, + originalEvent: {dataTransfer: {types: ['Files']}}, + preventDefault: $.noop + }); + }); + + module('Options', lifecycle); + + test('paramName', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + paramName: null, + send: function (e, data) { + strictEqual( + data.paramName[0], + data.fileInput.prop('name'), + 'Takes paramName from file input field if not set' + ); + return false; + } + }).fileupload('send', param); + }); + + test('url', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + url: null, + send: function (e, data) { + strictEqual( + data.url, + $(data.fileInput.prop('form')).prop('action'), + 'Takes url from form action if not set' + ); + return false; + } + }).fileupload('send', param); + }); + + test('type', function () { + expect(2); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + type: null, + send: function (e, data) { + strictEqual( + data.type, + 'POST', + 'Request type is "POST" if not set to "PUT"' + ); + return false; + } + }).fileupload('send', param); + $('#fileupload').fileupload({ + type: 'PUT', + send: function (e, data) { + strictEqual( + data.type, + 'PUT', + 'Request type is "PUT" if set to "PUT"' + ); + return false; + } + }).fileupload('send', param); + }); + + test('replaceFileInput', function () { + var fu = $('#fileupload').fileupload(), + fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'), + fileInput = fu.fileupload('option', 'fileInput'), + fileInputElement = fileInput[0]; + expect(2); + fu.fileupload({ + replaceFileInput: false, + change: function () { + strictEqual( + fu.fileupload('option', 'fileInput')[0], + fileInputElement, + 'Keeps file input with replaceFileInput: false' + ); + }, + add: $.noop + }); + fuo._onChange({ + data: {fileupload: fuo}, + target: fileInput[0] + }); + fu.fileupload({ + replaceFileInput: true, + change: function () { + notStrictEqual( + fu.fileupload('option', 'fileInput')[0], + fileInputElement, + 'Replaces file input with replaceFileInput: true' + ); + }, + add: $.noop + }); + fuo._onChange({ + data: {fileupload: fuo}, + target: fileInput[0] + }); + }); + + asyncTest('forceIframeTransport', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + forceIframeTransport: true, + done: function (e, data) { + strictEqual( + data.dataType.substr(0, 6), + 'iframe', + 'Iframe Transport is used' + ); + start(); + } + }).fileupload('send', param); + }); + + test('singleFileUploads', function () { + expect(3); + var fu = $('#fileupload').fileupload(), + param = {files: [{name: '1'}, {name: '2'}]}, + index = 1; + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + $('#fileupload').fileupload({ + singleFileUploads: true, + add: function () { + ok(true, 'Triggers callback number ' + index.toString()); + index += 1; + } + }).fileupload('add', param).fileupload( + 'option', + 'singleFileUploads', + false + ).fileupload('add', param); + }); + + test('limitMultiFileUploads', function () { + expect(3); + var fu = $('#fileupload').fileupload(), + param = {files: [ + {name: '1'}, + {name: '2'}, + {name: '3'}, + {name: '4'}, + {name: '5'} + ]}, + index = 1; + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + $('#fileupload').fileupload({ + singleFileUploads: false, + limitMultiFileUploads: 2, + add: function () { + ok(true, 'Triggers callback number ' + index.toString()); + index += 1; + } + }).fileupload('add', param); + }); + + test('limitMultiFileUploadSize', function () { + expect(7); + var fu = $('#fileupload').fileupload(), + param = {files: [ + {name: '1-1', size: 100000}, + {name: '1-2', size: 40000}, + {name: '2-1', size: 100000}, + {name: '3-1', size: 50000}, + {name: '3-2', size: 40000}, + {name: '4-1', size: 45000} // New request due to limitMultiFileUploads + ]}, + param2 = {files: [ + {name: '5-1'}, + {name: '5-2'}, + {name: '6-1'}, + {name: '6-2'}, + {name: '7-1'} + ]}, + index = 1; + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + $('#fileupload').fileupload({ + singleFileUploads: false, + limitMultiFileUploads: 2, + limitMultiFileUploadSize: 150000, + limitMultiFileUploadSizeOverhead: 5000, + add: function () { + ok(true, 'Triggers callback number ' + index.toString()); + index += 1; + } + }).fileupload('add', param).fileupload('add', param2); + }); + + asyncTest('sequentialUploads', function () { + expect(6); + var param = {files: [ + {name: '1'}, + {name: '2'}, + {name: '3'}, + {name: '4'}, + {name: '5'}, + {name: '6'} + ]}, + addIndex = 0, + sendIndex = 0, + loadIndex = 0, + fu = $('#fileupload').fileupload({ + sequentialUploads: true, + add: function (e, data) { + addIndex += 1; + if (addIndex === 4) { + data.submit().abort(); + } else { + data.submit(); + } + }, + send: function () { + sendIndex += 1; + }, + done: function () { + loadIndex += 1; + strictEqual(sendIndex, loadIndex, 'upload in order'); + }, + fail: function (e, data) { + strictEqual(data.errorThrown, 'abort', 'upload aborted'); + }, + stop: function () { + start(); + } + }); + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + fu.fileupload('add', param); + }); + + asyncTest('limitConcurrentUploads', function () { + expect(12); + var param = {files: [ + {name: '1'}, + {name: '2'}, + {name: '3'}, + {name: '4'}, + {name: '5'}, + {name: '6'}, + {name: '7'}, + {name: '8'}, + {name: '9'}, + {name: '10'}, + {name: '11'}, + {name: '12'} + ]}, + addIndex = 0, + sendIndex = 0, + loadIndex = 0, + fu = $('#fileupload').fileupload({ + limitConcurrentUploads: 3, + add: function (e, data) { + addIndex += 1; + if (addIndex === 4) { + data.submit().abort(); + } else { + data.submit(); + } + }, + send: function () { + sendIndex += 1; + }, + done: function () { + loadIndex += 1; + ok(sendIndex - loadIndex < 3); + }, + fail: function (e, data) { + strictEqual(data.errorThrown, 'abort', 'upload aborted'); + }, + stop: function () { + start(); + } + }); + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + fu.fileupload('add', param); + }); + + if ($.support.xhrFileUpload) { + asyncTest('multipart', function () { + expect(2); + var param = {files: [{ + name: 'test.png', + size: 123, + type: 'image/png' + }]}, + fu = $('#fileupload').fileupload({ + multipart: false, + always: function (e, data) { + strictEqual( + data.contentType, + param.files[0].type, + 'non-multipart upload sets file type as contentType' + ); + strictEqual( + data.headers['Content-Disposition'], + 'attachment; filename="' + param.files[0].name + '"', + 'non-multipart upload sets Content-Disposition header' + ); + start(); + } + }); + fu.fileupload('send', param); + }); + } + + module('UI Initialization', lifecycleUI); + + test('Widget initialization', function () { + var fu = $('#fileupload').fileupload(); + ok(fu.data('blueimp-fileupload') || fu.data('fileupload')); + ok( + $('#fileupload').fileupload('option', 'uploadTemplate').length, + 'Initialized upload template' + ); + ok( + $('#fileupload').fileupload('option', 'downloadTemplate').length, + 'Initialized download template' + ); + }); + + test('Buttonbar event listeners', function () { + var buttonbar = $('#fileupload .fileupload-buttonbar'), + files = [{name: 'test'}]; + expect(4); + $('#fileupload').fileupload({ + send: function () { + ok(true, 'Started file upload via global start button'); + }, + fail: function (e, data) { + ok(true, 'Canceled file upload via global cancel button'); + data.context.remove(); + }, + destroy: function () { + ok(true, 'Delete action called via global delete button'); + } + }); + $('#fileupload').fileupload('add', {files: files}); + buttonbar.find('.cancel').click(); + $('#fileupload').fileupload('add', {files: files}); + buttonbar.find('.start').click(); + buttonbar.find('.cancel').click(); + files[0].deleteUrl = 'http://example.org/banana.jpg'; + ($('#fileupload').data('blueimp-fileupload') || + $('#fileupload').data('fileupload')) + ._renderDownload(files) + .appendTo($('#fileupload .files')).show() + .find('.toggle').click(); + buttonbar.find('.delete').click(); + }); + + module('UI API', lifecycleUI); + + test('destroy', function () { + var buttonbar = $('#fileupload .fileupload-buttonbar'), + files = [{name: 'test'}]; + expect(1); + $('#fileupload').fileupload({ + send: function () { + ok(true, 'This test should not run'); + return false; + } + }) + .fileupload('add', {files: files}) + .fileupload('destroy'); + buttonbar.find('.start').click(function () { + ok(true, 'Clicked global start button'); + return false; + }).click(); + }); + + test('disable/enable', function () { + var buttonbar = $('#fileupload .fileupload-buttonbar'); + $('#fileupload').fileupload(); + $('#fileupload').fileupload('disable'); + strictEqual( + buttonbar.find('input[type=file], button').not(':disabled').length, + 0, + 'Disables the buttonbar buttons' + ); + $('#fileupload').fileupload('enable'); + strictEqual( + buttonbar.find('input[type=file], button').not(':disabled').length, + 4, + 'Enables the buttonbar buttons' + ); + }); + + module('UI Callbacks', lifecycleUI); + + test('destroy', function () { + expect(3); + $('#fileupload').fileupload({ + destroy: function (e, data) { + ok(true, 'Triggers destroy callback'); + strictEqual( + data.url, + 'test', + 'Passes over deletion url parameter' + ); + strictEqual( + data.type, + 'DELETE', + 'Passes over deletion request type parameter' + ); + } + }); + ($('#fileupload').data('blueimp-fileupload') || + $('#fileupload').data('fileupload')) + ._renderDownload([{ + name: 'test', + deleteUrl: 'test', + deleteType: 'DELETE' + }]) + .appendTo($('#fileupload .files')) + .show() + .find('.toggle').click(); + $('#fileupload .fileupload-buttonbar .delete').click(); + }); + + asyncTest('added', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + added: function (e, data) { + start(); + strictEqual( + data.files[0].name, + param.files[0].name, + 'Triggers added callback' + ); + }, + send: function () { + return false; + } + }).fileupload('add', param); + }); + + asyncTest('started', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + started: function () { + start(); + ok('Triggers started callback'); + return false; + }, + sent: function () { + return false; + } + }).fileupload('send', param); + }); + + asyncTest('sent', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + sent: function (e, data) { + start(); + strictEqual( + data.files[0].name, + param.files[0].name, + 'Triggers sent callback' + ); + return false; + } + }).fileupload('send', param); + }); + + asyncTest('completed', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + completed: function () { + start(); + ok('Triggers completed callback'); + return false; + } + }).fileupload('send', param); + }); + + asyncTest('failed', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + failed: function () { + start(); + ok('Triggers failed callback'); + return false; + } + }).fileupload('send', param).abort(); + }); + + asyncTest('stopped', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + stopped: function () { + start(); + ok('Triggers stopped callback'); + return false; + } + }).fileupload('send', param); + }); + + asyncTest('destroyed', function () { + expect(1); + $('#fileupload').fileupload({ + dataType: 'html', + destroyed: function () { + start(); + ok(true, 'Triggers destroyed callback'); + } + }); + ($('#fileupload').data('blueimp-fileupload') || + $('#fileupload').data('fileupload')) + ._renderDownload([{ + name: 'test', + deleteUrl: '.', + deleteType: 'GET' + }]) + .appendTo($('#fileupload .files')) + .show() + .find('.toggle').click(); + $('#fileupload .fileupload-buttonbar .delete').click(); + }); + + module('UI Options', lifecycleUI); + + test('autoUpload', function () { + expect(1); + $('#fileupload') + .fileupload({ + autoUpload: true, + send: function () { + ok(true, 'Started file upload automatically'); + return false; + } + }) + .fileupload('add', {files: [{name: 'test'}]}) + .fileupload('option', 'autoUpload', false) + .fileupload('add', {files: [{name: 'test'}]}); + }); + + test('maxNumberOfFiles', function () { + expect(3); + var addIndex = 0, + sendIndex = 0; + $('#fileupload') + .fileupload({ + autoUpload: true, + maxNumberOfFiles: 3, + singleFileUploads: false, + send: function () { + strictEqual( + sendIndex += 1, + addIndex + ); + }, + progress: $.noop, + progressall: $.noop, + done: $.noop, + stop: $.noop + }) + .fileupload('add', {files: [{name: (addIndex += 1)}]}) + .fileupload('add', {files: [{name: (addIndex += 1)}]}) + .fileupload('add', {files: [{name: (addIndex += 1)}]}) + .fileupload('add', {files: [{name: 'test'}]}); + }); + + test('maxFileSize', function () { + expect(2); + var addIndex = 0, + sendIndex = 0; + $('#fileupload') + .fileupload({ + autoUpload: true, + maxFileSize: 1000, + send: function () { + strictEqual( + sendIndex += 1, + addIndex + ); + return false; + } + }) + .fileupload('add', {files: [{ + name: (addIndex += 1) + }]}) + .fileupload('add', {files: [{ + name: (addIndex += 1), + size: 999 + }]}) + .fileupload('add', {files: [{ + name: 'test', + size: 1001 + }]}) + .fileupload({ + send: function (e, data) { + ok( + !$.blueimp.fileupload.prototype.options + .send.call(this, e, data) + ); + return false; + } + }); + }); + + test('minFileSize', function () { + expect(2); + var addIndex = 0, + sendIndex = 0; + $('#fileupload') + .fileupload({ + autoUpload: true, + minFileSize: 1000, + send: function () { + strictEqual( + sendIndex += 1, + addIndex + ); + return false; + } + }) + .fileupload('add', {files: [{ + name: (addIndex += 1) + }]}) + .fileupload('add', {files: [{ + name: (addIndex += 1), + size: 1001 + }]}) + .fileupload('add', {files: [{ + name: 'test', + size: 999 + }]}) + .fileupload({ + send: function (e, data) { + ok( + !$.blueimp.fileupload.prototype.options + .send.call(this, e, data) + ); + return false; + } + }); + }); + + test('acceptFileTypes', function () { + expect(2); + var addIndex = 0, + sendIndex = 0; + $('#fileupload') + .fileupload({ + autoUpload: true, + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, + disableImageMetaDataLoad: true, + send: function () { + strictEqual( + sendIndex += 1, + addIndex + ); + return false; + } + }) + .fileupload('add', {files: [{ + name: (addIndex += 1) + '.jpg' + }]}) + .fileupload('add', {files: [{ + name: (addIndex += 1), + type: 'image/jpeg' + }]}) + .fileupload('add', {files: [{ + name: 'test.txt', + type: 'text/plain' + }]}) + .fileupload({ + send: function (e, data) { + ok( + !$.blueimp.fileupload.prototype.options + .send.call(this, e, data) + ); + return false; + } + }); + }); + + test('acceptFileTypes as HTML5 data attribute', function () { + expect(2); + var regExp = /(\.|\/)(gif|jpe?g|png)$/i; + $('#fileupload') + .attr('data-accept-file-types', regExp.toString()) + .fileupload(); + strictEqual( + $.type($('#fileupload').fileupload('option', 'acceptFileTypes')), + $.type(regExp) + ); + strictEqual( + $('#fileupload').fileupload('option', 'acceptFileTypes').toString(), + regExp.toString() + ); + }); + +}); diff --git a/apps/files/static/files/jquery.fileupload.js b/apps/files/static/files/jquery.fileupload.js new file mode 100644 index 0000000..7929daa --- /dev/null +++ b/apps/files/static/files/jquery.fileupload.js @@ -0,0 +1,1486 @@ +/* + * jQuery File Upload Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, document, location, Blob, FormData */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'jquery-ui/ui/widget' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('./vendor/jquery.ui.widget') + ); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + // Detect file input support, based on + // http://viljamis.com/blog/2012/file-upload-support-on-mobile/ + $.support.fileInput = !(new RegExp( + // Handle devices which give false positives for the feature detection: + '(Android (1\\.[0156]|2\\.[01]))' + + '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + + '|(w(eb)?OSBrowser)|(webOS)' + + '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' + ).test(window.navigator.userAgent) || + // Feature detection for all other devices: + $('').prop('disabled')); + + // The FileReader API is not actually used, but works as feature detection, + // as some Safari versions (5?) support XHR file uploads via the FormData API, + // but not non-multipart XHR file uploads. + // window.XMLHttpRequestUpload is not available on IE10, so we check for + // window.ProgressEvent instead to detect XHR2 file upload capability: + $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); + $.support.xhrFormDataFileUpload = !!window.FormData; + + // Detect support for Blob slicing (required for chunked uploads): + $.support.blobSlice = window.Blob && (Blob.prototype.slice || + Blob.prototype.webkitSlice || Blob.prototype.mozSlice); + + // Helper function to create drag handlers for dragover/dragenter/dragleave: + function getDragHandler(type) { + var isDragOver = type === 'dragover'; + return function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; + if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 && + this._trigger( + type, + $.Event(type, {delegatedEvent: e}) + ) !== false) { + e.preventDefault(); + if (isDragOver) { + dataTransfer.dropEffect = 'copy'; + } + } + }; + } + + // The fileupload widget listens for change events on file input fields defined + // via fileInput setting and paste or drop events of the given dropZone. + // In addition to the default jQuery Widget methods, the fileupload widget + // exposes the "add" and "send" methods, to add or directly send files using + // the fileupload API. + // By default, files added via file input selection, paste, drag & drop or + // "add" method are uploaded immediately, but it is possible to override + // the "add" callback option to queue file uploads. + $.widget('blueimp.fileupload', { + + options: { + // The drop target element(s), by the default the complete document. + // Set to null to disable drag & drop support: + dropZone: $(document), + // The paste target element(s), by the default undefined. + // Set to a DOM node or jQuery object to enable file pasting: + pasteZone: undefined, + // The file input field(s), that are listened to for change events. + // If undefined, it is set to the file input fields inside + // of the widget element on plugin initialization. + // Set to null to disable the change listener. + fileInput: undefined, + // By default, the file input field is replaced with a clone after + // each input field change event. This is required for iframe transport + // queues and allows change events to be fired for the same file + // selection, but can be disabled by setting the following option to false: + replaceFileInput: true, + // The parameter name for the file form data (the request argument name). + // If undefined or empty, the name property of the file input field is + // used, or "files[]" if the file input name property is also empty, + // can be a string or an array of strings: + paramName: undefined, + // By default, each file of a selection is uploaded using an individual + // request for XHR type uploads. Set to false to upload file + // selections in one request each: + singleFileUploads: true, + // To limit the number of files uploaded with one XHR request, + // set the following option to an integer greater than 0: + limitMultiFileUploads: undefined, + // The following option limits the number of files uploaded with one + // XHR request to keep the request size under or equal to the defined + // limit in bytes: + limitMultiFileUploadSize: undefined, + // Multipart file uploads add a number of bytes to each uploaded file, + // therefore the following option adds an overhead for each file used + // in the limitMultiFileUploadSize configuration: + limitMultiFileUploadSizeOverhead: 512, + // Set the following option to true to issue all file upload requests + // in a sequential order: + sequentialUploads: false, + // To limit the number of concurrent uploads, + // set the following option to an integer greater than 0: + limitConcurrentUploads: undefined, + // Set the following option to true to force iframe transport uploads: + forceIframeTransport: false, + // Set the following option to the location of a redirect url on the + // origin server, for cross-domain iframe transport uploads: + redirect: undefined, + // The parameter name for the redirect url, sent as part of the form + // data and set to 'redirect' if this option is empty: + redirectParamName: undefined, + // Set the following option to the location of a postMessage window, + // to enable postMessage transport uploads: + postMessage: undefined, + // By default, XHR file uploads are sent as multipart/form-data. + // The iframe transport is always using multipart/form-data. + // Set to false to enable non-multipart XHR uploads: + multipart: true, + // To upload large files in smaller chunks, set the following option + // to a preferred maximum chunk size. If set to 0, null or undefined, + // or the browser does not support the required Blob API, files will + // be uploaded as a whole. + maxChunkSize: undefined, + // When a non-multipart upload or a chunked multipart upload has been + // aborted, this option can be used to resume the upload by setting + // it to the size of the already uploaded bytes. This option is most + // useful when modifying the options object inside of the "add" or + // "send" callbacks, as the options are cloned for each file upload. + uploadedBytes: undefined, + // By default, failed (abort or error) file uploads are removed from the + // global progress calculation. Set the following option to false to + // prevent recalculating the global progress data: + recalculateProgress: true, + // Interval in milliseconds to calculate and trigger progress events: + progressInterval: 100, + // Interval in milliseconds to calculate progress bitrate: + bitrateInterval: 500, + // By default, uploads are started automatically when adding files: + autoUpload: true, + + // Error and info messages: + messages: { + uploadedBytes: 'Uploaded bytes exceed file size' + }, + + // Translation function, gets the message key to be translated + // and an object with context specific data as arguments: + i18n: function (message, context) { + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + message = message.replace('{' + key + '}', value); + }); + } + return message; + }, + + // Additional form data to be sent along with the file uploads can be set + // using this option, which accepts an array of objects with name and + // value properties, a function returning such an array, a FormData + // object (for XHR file uploads), or a simple object. + // The form of the first fileInput is given as parameter to the function: + formData: function (form) { + return form.serializeArray(); + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop, paste or add API call). + // If the singleFileUploads option is enabled, this callback will be + // called once for each file in the selection for XHR file uploads, else + // once for each file selection. + // + // The upload starts when the submit method is invoked on the data parameter. + // The data object contains a files property holding the added files + // and allows you to override plugin options as well as define ajax settings. + // + // Listeners for this callback can also be bound the following way: + // .bind('fileuploadadd', func); + // + // data.submit() returns a Promise object and allows to attach additional + // handlers using jQuery's Deferred callbacks: + // data.submit().done(func).fail(func).always(func); + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + if (data.autoUpload || (data.autoUpload !== false && + $(this).fileupload('option', 'autoUpload'))) { + data.process().done(function () { + data.submit(); + }); + } + }, + + // Other callbacks: + + // Callback for the submit event of each file upload: + // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); + + // Callback for the start of each file upload request: + // send: function (e, data) {}, // .bind('fileuploadsend', func); + + // Callback for successful uploads: + // done: function (e, data) {}, // .bind('fileuploaddone', func); + + // Callback for failed (abort or error) uploads: + // fail: function (e, data) {}, // .bind('fileuploadfail', func); + + // Callback for completed (success, abort or error) requests: + // always: function (e, data) {}, // .bind('fileuploadalways', func); + + // Callback for upload progress events: + // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + + // Callback for global upload progress events: + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + + // Callback for uploads start, equivalent to the global ajaxStart event: + // start: function (e) {}, // .bind('fileuploadstart', func); + + // Callback for uploads stop, equivalent to the global ajaxStop event: + // stop: function (e) {}, // .bind('fileuploadstop', func); + + // Callback for change events of the fileInput(s): + // change: function (e, data) {}, // .bind('fileuploadchange', func); + + // Callback for paste events to the pasteZone(s): + // paste: function (e, data) {}, // .bind('fileuploadpaste', func); + + // Callback for drop events of the dropZone(s): + // drop: function (e, data) {}, // .bind('fileuploaddrop', func); + + // Callback for dragover events of the dropZone(s): + // dragover: function (e) {}, // .bind('fileuploaddragover', func); + + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); + + // The plugin options are used as settings object for the ajax calls. + // The following are jQuery ajax settings required for the file uploads: + processData: false, + contentType: false, + cache: false, + timeout: 0 + }, + + // A list of options that require reinitializing event listeners and/or + // special initialization code: + _specialOptions: [ + 'fileInput', + 'dropZone', + 'pasteZone', + 'multipart', + 'forceIframeTransport' + ], + + _blobSlice: $.support.blobSlice && function () { + var slice = this.slice || this.webkitSlice || this.mozSlice; + return slice.apply(this, arguments); + }, + + _BitrateTimer: function () { + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); + this.loaded = 0; + this.bitrate = 0; + this.getBitrate = function (now, loaded, interval) { + var timeDiff = now - this.timestamp; + if (!this.bitrate || !interval || timeDiff > interval) { + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; + this.loaded = loaded; + this.timestamp = now; + } + return this.bitrate; + }; + }, + + _isXHRUpload: function (options) { + return !options.forceIframeTransport && + ((!options.multipart && $.support.xhrFileUpload) || + $.support.xhrFormDataFileUpload); + }, + + _getFormData: function (options) { + var formData; + if ($.type(options.formData) === 'function') { + return options.formData(options.form); + } + if ($.isArray(options.formData)) { + return options.formData; + } + if ($.type(options.formData) === 'object') { + formData = []; + $.each(options.formData, function (name, value) { + formData.push({name: name, value: value}); + }); + return formData; + } + return []; + }, + + _getTotal: function (files) { + var total = 0; + $.each(files, function (index, file) { + total += file.size || 1; + }); + return total; + }, + + _initProgressObject: function (obj) { + var progress = { + loaded: 0, + total: 0, + bitrate: 0 + }; + if (obj._progress) { + $.extend(obj._progress, progress); + } else { + obj._progress = progress; + } + }, + + _initResponseObject: function (obj) { + var prop; + if (obj._response) { + for (prop in obj._response) { + if (obj._response.hasOwnProperty(prop)) { + delete obj._response[prop]; + } + } + } else { + obj._response = {}; + } + }, + + _onProgress: function (e, data) { + if (e.lengthComputable) { + var now = ((Date.now) ? Date.now() : (new Date()).getTime()), + loaded; + if (data._time && data.progressInterval && + (now - data._time < data.progressInterval) && + e.loaded !== e.total) { + return; + } + data._time = now; + loaded = Math.floor( + e.loaded / e.total * (data.chunkSize || data._progress.total) + ) + (data.uploadedBytes || 0); + // Add the difference from the previously loaded state + // to the global loaded counter: + this._progress.loaded += (loaded - data._progress.loaded); + this._progress.bitrate = this._bitrateTimer.getBitrate( + now, + this._progress.loaded, + data.bitrateInterval + ); + data._progress.loaded = data.loaded = loaded; + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( + now, + loaded, + data.bitrateInterval + ); + // Trigger a custom progress event with a total data property set + // to the file size(s) of the current upload and a loaded data + // property calculated accordingly: + this._trigger( + 'progress', + $.Event('progress', {delegatedEvent: e}), + data + ); + // Trigger a global progress event for all current file uploads, + // including ajax calls queued for sequential file uploads: + this._trigger( + 'progressall', + $.Event('progressall', {delegatedEvent: e}), + this._progress + ); + } + }, + + _initProgressListener: function (options) { + var that = this, + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + // Accesss to the native XHR object is required to add event listeners + // for the upload progress event: + if (xhr.upload) { + $(xhr.upload).bind('progress', function (e) { + var oe = e.originalEvent; + // Make sure the progress event properties get copied over: + e.lengthComputable = oe.lengthComputable; + e.loaded = oe.loaded; + e.total = oe.total; + that._onProgress(e, options); + }); + options.xhr = function () { + return xhr; + }; + } + }, + + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + + _initXHRData: function (options) { + var that = this, + formData, + file = options.files[0], + // Ignore non-multipart setting if not supported: + multipart = options.multipart || !$.support.xhrFileUpload, + paramName = $.type(options.paramName) === 'array' ? + options.paramName[0] : options.paramName; + options.headers = $.extend({}, options.headers); + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; + } + if (!multipart || options.blob || !this._isInstanceOf('File', file)) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.uploadName || file.name) + '"'; + } + if (!multipart) { + options.contentType = file.type || 'application/octet-stream'; + options.data = options.blob || file; + } else if ($.support.xhrFormDataFileUpload) { + if (options.postMessage) { + // window.postMessage does not allow sending FormData + // objects, so we just add the File/Blob objects to + // the formData array and let the postMessage window + // create the FormData object out of this array: + formData = this._getFormData(options); + if (options.blob) { + formData.push({ + name: paramName, + value: options.blob + }); + } else { + $.each(options.files, function (index, file) { + formData.push({ + name: ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, + value: file + }); + }); + } + } else { + if (that._isInstanceOf('FormData', options.formData)) { + formData = options.formData; + } else { + formData = new FormData(); + $.each(this._getFormData(options), function (index, field) { + formData.append(field.name, field.value); + }); + } + if (options.blob) { + formData.append( + paramName, + options.blob, + file.uploadName || file.name + ); + } else { + $.each(options.files, function (index, file) { + // This check allows the tests to run with + // dummy objects: + if (that._isInstanceOf('File', file) || + that._isInstanceOf('Blob', file)) { + formData.append( + ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, + file, + file.uploadName || file.name + ); + } + }); + } + } + options.data = formData; + } + // Blob reference is not needed anymore, free memory: + options.blob = null; + }, + + _initIframeSettings: function (options) { + var targetHost = $('
    ').prop('href', options.url).prop('host'); + // Setting the dataType to iframe enables the iframe transport: + options.dataType = 'iframe ' + (options.dataType || ''); + // The iframe transport accepts a serialized array as form data: + options.formData = this._getFormData(options); + // Add redirect url to form data on cross-domain uploads: + if (options.redirect && targetHost && targetHost !== location.host) { + options.formData.push({ + name: options.redirectParamName || 'redirect', + value: options.redirect + }); + } + }, + + _initDataSettings: function (options) { + if (this._isXHRUpload(options)) { + if (!this._chunkedUpload(options, true)) { + if (!options.data) { + this._initXHRData(options); + } + this._initProgressListener(options); + } + if (options.postMessage) { + // Setting the dataType to postmessage enables the + // postMessage transport: + options.dataType = 'postmessage ' + (options.dataType || ''); + } + } else { + this._initIframeSettings(options); + } + }, + + _getParamName: function (options) { + var fileInput = $(options.fileInput), + paramName = options.paramName; + if (!paramName) { + paramName = []; + fileInput.each(function () { + var input = $(this), + name = input.prop('name') || 'files[]', + i = (input.prop('files') || [1]).length; + while (i) { + paramName.push(name); + i -= 1; + } + }); + if (!paramName.length) { + paramName = [fileInput.prop('name') || 'files[]']; + } + } else if (!$.isArray(paramName)) { + paramName = [paramName]; + } + return paramName; + }, + + _initFormSettings: function (options) { + // Retrieve missing options from the input field and the + // associated form, if available: + if (!options.form || !options.form.length) { + options.form = $(options.fileInput.prop('form')); + // If the given file input doesn't have an associated form, + // use the default widget file input's form: + if (!options.form.length) { + options.form = $(this.options.fileInput.prop('form')); + } + } + options.paramName = this._getParamName(options); + if (!options.url) { + options.url = options.form.prop('action') || location.href; + } + // The HTTP request method must be "POST" or "PUT": + options.type = (options.type || + ($.type(options.form.prop('method')) === 'string' && + options.form.prop('method')) || '' + ).toUpperCase(); + if (options.type !== 'POST' && options.type !== 'PUT' && + options.type !== 'PATCH') { + options.type = 'POST'; + } + if (!options.formAcceptCharset) { + options.formAcceptCharset = options.form.attr('accept-charset'); + } + }, + + _getAJAXSettings: function (data) { + var options = $.extend({}, this.options, data); + this._initFormSettings(options); + this._initDataSettings(options); + return options; + }, + + // jQuery 1.6 doesn't provide .state(), + // while jQuery 1.8+ removed .isRejected() and .isResolved(): + _getDeferredState: function (deferred) { + if (deferred.state) { + return deferred.state(); + } + if (deferred.isResolved()) { + return 'resolved'; + } + if (deferred.isRejected()) { + return 'rejected'; + } + return 'pending'; + }, + + // Maps jqXHR callbacks to the equivalent + // methods of the given Promise object: + _enhancePromise: function (promise) { + promise.success = promise.done; + promise.error = promise.fail; + promise.complete = promise.always; + return promise; + }, + + // Creates and returns a Promise object enhanced with + // the jqXHR methods abort, success, error and complete: + _getXHRPromise: function (resolveOrReject, context, args) { + var dfd = $.Deferred(), + promise = dfd.promise(); + context = context || this.options.context || promise; + if (resolveOrReject === true) { + dfd.resolveWith(context, args); + } else if (resolveOrReject === false) { + dfd.rejectWith(context, args); + } + promise.abort = dfd.promise; + return this._enhancePromise(promise); + }, + + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (args) { + return $.Deferred().resolveWith(that, args).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = + (this._processQueue || getPromise([this])).then( + function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return getPromise(arguments); + } + ).then(resolveFunc, rejectFunc); + } + return this._processQueue || getPromise([this]); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + (that._trigger( + 'submit', + $.Event('submit', {delegatedEvent: e}), + this + ) !== false) && that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + this.errorThrown = 'abort'; + that._trigger('fail', null, this); + return that._getXHRPromise(false); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + data.processing = function () { + return !this.jqXHR && this._processQueue && that + ._getDeferredState(this._processQueue) === 'pending'; + }; + data.progress = function () { + return this._progress; + }; + data.response = function () { + return this._response; + }; + }, + + // Parses the Range header from the server response + // and returns the uploaded bytes: + _getUploadedBytes: function (jqXHR) { + var range = jqXHR.getResponseHeader('Range'), + parts = range && range.split('-'), + upperBytesPos = parts && parts.length > 1 && + parseInt(parts[1], 10); + return upperBytesPos && upperBytesPos + 1; + }, + + // Uploads a file in multiple, sequential requests + // by splitting the file up in multiple blob chunks. + // If the second parameter is true, only tests if the file + // should be uploaded in chunks, but does not invoke any + // upload requests: + _chunkedUpload: function (options, testOnly) { + options.uploadedBytes = options.uploadedBytes || 0; + var that = this, + file = options.files[0], + fs = file.size, + ub = options.uploadedBytes, + mcs = options.maxChunkSize || fs, + slice = this._blobSlice, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + upload; + if (!(this._isXHRUpload(options) && slice && (ub || ($.type(mcs) === 'function' ? mcs(options) : mcs) < fs)) || + options.data) { + return false; + } + if (testOnly) { + return true; + } + if (ub >= fs) { + file.error = options.i18n('uploadedBytes'); + return this._getXHRPromise( + false, + options.context, + [null, 'error', file.error] + ); + } + // The chunk upload method: + upload = function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options), + currentLoaded = o._progress.loaded; + o.blob = slice.call( + file, + ub, + ub + ($.type(mcs) === 'function' ? mcs(o) : mcs), + file.type + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Expose the chunk bytes position range: + o.contentRange = 'bytes ' + ub + '-' + + (ub + o.chunkSize - 1) + '/' + fs; + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || + that._getXHRPromise(false, o.context)) + .done(function (result, textStatus, jqXHR) { + ub = that._getUploadedBytes(jqXHR) || + (ub + o.chunkSize); + // Create a progress event if no final progress event + // with loaded equaling total has been triggered + // for this chunk: + if (currentLoaded + o.chunkSize - o._progress.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: ub - o.uploadedBytes, + total: ub - o.uploadedBytes + }), o); + } + options.uploadedBytes = o.uploadedBytes = ub; + o.result = result; + o.textStatus = textStatus; + o.jqXHR = jqXHR; + that._trigger('chunkdone', null, o); + that._trigger('chunkalways', null, o); + if (ub < fs) { + // File upload not yet complete, + // continue with the next chunk: + upload(); + } else { + dfd.resolveWith( + o.context, + [result, textStatus, jqXHR] + ); + } + }) + .fail(function (jqXHR, textStatus, errorThrown) { + o.jqXHR = jqXHR; + o.textStatus = textStatus; + o.errorThrown = errorThrown; + that._trigger('chunkfail', null, o); + that._trigger('chunkalways', null, o); + dfd.rejectWith( + o.context, + [jqXHR, textStatus, errorThrown] + ); + }); + }; + this._enhancePromise(promise); + promise.abort = function () { + return jqXHR.abort(); + }; + upload(); + return promise; + }, + + _beforeSend: function (e, data) { + if (this._active === 0) { + // the start callback is triggered when an upload starts + // and no other uploads are currently running, + // equivalent to the global ajaxStart event: + this._trigger('start'); + // Set timer for global bitrate progress calculation: + this._bitrateTimer = new this._BitrateTimer(); + // Reset the global progress values: + this._progress.loaded = this._progress.total = 0; + this._progress.bitrate = 0; + } + // Make sure the container objects for the .response() and + // .progress() methods on the data object are available + // and reset to their initial state: + this._initResponseObject(data); + this._initProgressObject(data); + data._progress.loaded = data.loaded = data.uploadedBytes || 0; + data._progress.total = data.total = this._getTotal(data.files) || 1; + data._progress.bitrate = data.bitrate = 0; + this._active += 1; + // Initialize the global progress values: + this._progress.loaded += data.loaded; + this._progress.total += data.total; + }, + + _onDone: function (result, textStatus, jqXHR, options) { + var total = options._progress.total, + response = options._response; + if (options._progress.loaded < total) { + // Create a progress event if no final progress event + // with loaded equaling total has been triggered: + this._onProgress($.Event('progress', { + lengthComputable: true, + loaded: total, + total: total + }), options); + } + response.result = options.result = result; + response.textStatus = options.textStatus = textStatus; + response.jqXHR = options.jqXHR = jqXHR; + this._trigger('done', null, options); + }, + + _onFail: function (jqXHR, textStatus, errorThrown, options) { + var response = options._response; + if (options.recalculateProgress) { + // Remove the failed (error or abort) file upload from + // the global progress calculation: + this._progress.loaded -= options._progress.loaded; + this._progress.total -= options._progress.total; + } + response.jqXHR = options.jqXHR = jqXHR; + response.textStatus = options.textStatus = textStatus; + response.errorThrown = options.errorThrown = errorThrown; + this._trigger('fail', null, options); + }, + + _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { + // jqXHRorResult, textStatus and jqXHRorError are added to the + // options object via done and fail callbacks + this._trigger('always', null, options); + }, + + _onSend: function (e, data) { + if (!data.submit) { + this._addConvenienceMethods(e, data); + } + var that = this, + jqXHR, + aborted, + slot, + pipe, + options = that._getAJAXSettings(data), + send = function () { + that._sending += 1; + // Set timer for bitrate progress calculation: + options._bitrateTimer = new that._BitrateTimer(); + jqXHR = jqXHR || ( + ((aborted || that._trigger( + 'send', + $.Event('send', {delegatedEvent: e}), + options + ) === false) && + that._getXHRPromise(false, options.context, aborted)) || + that._chunkedUpload(options) || $.ajax(options) + ).done(function (result, textStatus, jqXHR) { + that._onDone(result, textStatus, jqXHR, options); + }).fail(function (jqXHR, textStatus, errorThrown) { + that._onFail(jqXHR, textStatus, errorThrown, options); + }).always(function (jqXHRorResult, textStatus, jqXHRorError) { + that._onAlways( + jqXHRorResult, + textStatus, + jqXHRorError, + options + ); + that._sending -= 1; + that._active -= 1; + if (options.limitConcurrentUploads && + options.limitConcurrentUploads > that._sending) { + // Start the next queued upload, + // that has not been aborted: + var nextSlot = that._slots.shift(); + while (nextSlot) { + if (that._getDeferredState(nextSlot) === 'pending') { + nextSlot.resolve(); + break; + } + nextSlot = that._slots.shift(); + } + } + if (that._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + that._trigger('stop'); + } + }); + return jqXHR; + }; + this._beforeSend(e, options); + if (this.options.sequentialUploads || + (this.options.limitConcurrentUploads && + this.options.limitConcurrentUploads <= this._sending)) { + if (this.options.limitConcurrentUploads > 1) { + slot = $.Deferred(); + this._slots.push(slot); + pipe = slot.then(send); + } else { + this._sequence = this._sequence.then(send, send); + pipe = this._sequence; + } + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe.abort = function () { + aborted = [undefined, 'abort', 'abort']; + if (!jqXHR) { + if (slot) { + slot.rejectWith(options.context, aborted); + } + return send(); + } + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + } + return send(); + }, + + _onAdd: function (e, data) { + var that = this, + result = true, + options = $.extend({}, this.options, data), + files = data.files, + filesLength = files.length, + limit = options.limitMultiFileUploads, + limitSize = options.limitMultiFileUploadSize, + overhead = options.limitMultiFileUploadSizeOverhead, + batchSize = 0, + paramName = this._getParamName(options), + paramNameSet, + paramNameSlice, + fileSet, + i, + j = 0; + if (!filesLength) { + return false; + } + if (limitSize && files[0].size === undefined) { + limitSize = undefined; + } + if (!(options.singleFileUploads || limit || limitSize) || + !this._isXHRUpload(options)) { + fileSet = [files]; + paramNameSet = [paramName]; + } else if (!(options.singleFileUploads || limitSize) && limit) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i += limit) { + fileSet.push(files.slice(i, i + limit)); + paramNameSlice = paramName.slice(i, i + limit); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + } + } else if (!options.singleFileUploads && limitSize) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i = i + 1) { + batchSize += files[i].size + overhead; + if (i + 1 === filesLength || + ((batchSize + files[i + 1].size + overhead) > limitSize) || + (limit && i + 1 - j >= limit)) { + fileSet.push(files.slice(j, i + 1)); + paramNameSlice = paramName.slice(j, i + 1); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + j = i + 1; + batchSize = 0; + } + } + } else { + paramNameSet = paramName; + } + data.originalFiles = files; + $.each(fileSet || files, function (index, element) { + var newData = $.extend({}, data); + newData.files = fileSet ? element : [element]; + newData.paramName = paramNameSet[index]; + that._initResponseObject(newData); + that._initProgressObject(newData); + that._addConvenienceMethods(e, newData); + result = that._trigger( + 'add', + $.Event('add', {delegatedEvent: e}), + newData + ); + return result; + }); + return result; + }, + + _replaceFileInput: function (data) { + var input = data.fileInput, + inputClone = input.clone(true), + restoreFocus = input.is(document.activeElement); + // Add a reference for the new cloned file input to the data argument: + data.fileInputClone = inputClone; + $('
    ').append(inputClone)[0].reset(); + // Detaching allows to insert the fileInput on another form + // without loosing the file input value: + input.after(inputClone).detach(); + // If the fileInput had focus before it was detached, + // restore focus to the inputClone. + if (restoreFocus) { + inputClone.focus(); + } + // Avoid memory leaks with the detached file input: + $.cleanData(input.unbind('remove')); + // Replace the original file input element in the fileInput + // elements set with the clone, which has been copied including + // event handlers: + this.options.fileInput = this.options.fileInput.map(function (i, el) { + if (el === input[0]) { + return inputClone[0]; + } + return el; + }); + // If the widget has been initialized on the file input itself, + // override this.element with the file input clone: + if (input[0] === this.element[0]) { + this.element = inputClone; + } + }, + + _handleFileTreeEntry: function (entry, path) { + var that = this, + dfd = $.Deferred(), + entries = [], + dirReader, + errorHandler = function (e) { + if (e && !e.entry) { + e.entry = entry; + } + // Since $.when returns immediately if one + // Deferred is rejected, we use resolve instead. + // This allows valid files and invalid items + // to be returned together in one set: + dfd.resolve([e]); + }, + successHandler = function (entries) { + that._handleFileTreeEntries( + entries, + path + entry.name + '/' + ).done(function (files) { + dfd.resolve(files); + }).fail(errorHandler); + }, + readEntries = function () { + dirReader.readEntries(function (results) { + if (!results.length) { + successHandler(entries); + } else { + entries = entries.concat(results); + readEntries(); + } + }, errorHandler); + }; + path = path || ''; + if (entry.isFile) { + if (entry._file) { + // Workaround for Chrome bug #149735 + entry._file.relativePath = path; + dfd.resolve(entry._file); + } else { + entry.file(function (file) { + file.relativePath = path; + dfd.resolve(file); + }, errorHandler); + } + } else if (entry.isDirectory) { + dirReader = entry.createReader(); + readEntries(); + } else { + // Return an empy list for file system items + // other than files or directories: + dfd.resolve([]); + } + return dfd.promise(); + }, + + _handleFileTreeEntries: function (entries, path) { + var that = this; + return $.when.apply( + $, + $.map(entries, function (entry) { + return that._handleFileTreeEntry(entry, path); + }) + ).then(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _getDroppedFiles: function (dataTransfer) { + dataTransfer = dataTransfer || {}; + var items = dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry || + items[0].getAsEntry)) { + return this._handleFileTreeEntries( + $.map(items, function (item) { + var entry; + if (item.webkitGetAsEntry) { + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; + } + return item.getAsEntry(); + }) + ); + } + return $.Deferred().resolve( + $.makeArray(dataTransfer.files) + ).promise(); + }, + + _getSingleFileInputFiles: function (fileInput) { + fileInput = $(fileInput); + var entries = fileInput.prop('webkitEntries') || + fileInput.prop('entries'), + files, + value; + if (entries && entries.length) { + return this._handleFileTreeEntries(entries); + } + files = $.makeArray(fileInput.prop('files')); + if (!files.length) { + value = fileInput.prop('value'); + if (!value) { + return $.Deferred().resolve([]).promise(); + } + // If the files property is not available, the browser does not + // support the File API and we add a pseudo File object with + // the input value as name with path information removed: + files = [{name: value.replace(/^.*\\/, '')}]; + } else if (files[0].name === undefined && files[0].fileName) { + // File normalization for Safari 4 and Firefox 3: + $.each(files, function (index, file) { + file.name = file.fileName; + file.size = file.fileSize; + }); + } + return $.Deferred().resolve(files).promise(); + }, + + _getFileInputFiles: function (fileInput) { + if (!(fileInput instanceof $) || fileInput.length === 1) { + return this._getSingleFileInputFiles(fileInput); + } + return $.when.apply( + $, + $.map(fileInput, this._getSingleFileInputFiles) + ).then(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _onChange: function (e) { + var that = this, + data = { + fileInput: $(e.target), + form: $(e.target.form) + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + if (that.options.replaceFileInput) { + that._replaceFileInput(data); + } + if (that._trigger( + 'change', + $.Event('change', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); + }, + + _onPaste: function (e) { + var items = e.originalEvent && e.originalEvent.clipboardData && + e.originalEvent.clipboardData.items, + data = {files: []}; + if (items && items.length) { + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if (this._trigger( + 'paste', + $.Event('paste', {delegatedEvent: e}), + data + ) !== false) { + this._onAdd(e, data); + } + } + }, + + _onDrop: function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var that = this, + dataTransfer = e.dataTransfer, + data = {}; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if (that._trigger( + 'drop', + $.Event('drop', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); + } + }, + + _onDragOver: getDragHandler('dragover'), + + _onDragEnter: getDragHandler('dragenter'), + + _onDragLeave: getDragHandler('dragleave'), + + _initEventHandlers: function () { + if (this._isXHRUpload(this.options)) { + this._on(this.options.dropZone, { + dragover: this._onDragOver, + drop: this._onDrop, + // event.preventDefault() on dragenter is required for IE10+: + dragenter: this._onDragEnter, + // dragleave is not required, but added for completeness: + dragleave: this._onDragLeave + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); + } + if ($.support.fileInput) { + this._on(this.options.fileInput, { + change: this._onChange + }); + } + }, + + _destroyEventHandlers: function () { + this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); + this._off(this.options.pasteZone, 'paste'); + this._off(this.options.fileInput, 'change'); + }, + + _destroy: function () { + this._destroyEventHandlers(); + }, + + _setOption: function (key, value) { + var reinit = $.inArray(key, this._specialOptions) !== -1; + if (reinit) { + this._destroyEventHandlers(); + } + this._super(key, value); + if (reinit) { + this._initSpecialOptions(); + this._initEventHandlers(); + } + }, + + _initSpecialOptions: function () { + var options = this.options; + if (options.fileInput === undefined) { + options.fileInput = this.element.is('input[type="file"]') ? + this.element : this.element.find('input[type="file"]'); + } else if (!(options.fileInput instanceof $)) { + options.fileInput = $(options.fileInput); + } + if (!(options.dropZone instanceof $)) { + options.dropZone = $(options.dropZone); + } + if (!(options.pasteZone instanceof $)) { + options.pasteZone = $(options.pasteZone); + } + }, + + _getRegExp: function (str) { + var parts = str.split('/'), + modifiers = parts.pop(); + parts.shift(); + return new RegExp(parts.join('/'), modifiers); + }, + + _isRegExpOption: function (key, value) { + return key !== 'url' && $.type(value) === 'string' && + /^\/.*\/[igm]{0,3}$/.test(value); + }, + + _initDataAttributes: function () { + var that = this, + options = this.options, + data = this.element.data(); + // Initialize options set via HTML5 data-attributes: + $.each( + this.element[0].attributes, + function (index, attr) { + var key = attr.name.toLowerCase(), + value; + if (/^data-/.test(key)) { + // Convert hyphen-ated key to camelCase: + key = key.slice(5).replace(/-[a-z]/g, function (str) { + return str.charAt(1).toUpperCase(); + }); + value = data[key]; + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; + } + } + ); + }, + + _create: function () { + this._initDataAttributes(); + this._initSpecialOptions(); + this._slots = []; + this._sequence = this._getXHRPromise(true); + this._sending = this._active = 0; + this._initProgressObject(this); + this._initEventHandlers(); + }, + + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; + }, + + // This method is exposed to the widget API and allows to query + // the widget upload progress. + // It returns an object with loaded, total and bitrate properties + // for the running uploads: + progress: function () { + return this._progress; + }, + + // This method is exposed to the widget API and allows adding files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('add', {files: filesList}); + add: function (data) { + var that = this; + if (!data || this.options.disabled) { + return; + } + if (data.fileInput && !data.files) { + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + that._onAdd(null, data); + }); + } else { + data.files = $.makeArray(data.files); + this._onAdd(null, data); + } + }, + + // This method is exposed to the widget API and allows sending files + // using the fileupload API. The data parameter accepts an object which + // must have a files or fileInput property and can contain additional options: + // .fileupload('send', {files: filesList}); + // The method returns a Promise object for the file upload call. + send: function (data) { + if (data && !this.options.disabled) { + if (data.fileInput && !data.files) { + var that = this, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + aborted; + promise.abort = function () { + aborted = true; + if (jqXHR) { + return jqXHR.abort(); + } + dfd.reject(null, 'abort', 'abort'); + return promise; + }; + this._getFileInputFiles(data.fileInput).always( + function (files) { + if (aborted) { + return; + } + if (!files.length) { + dfd.reject(); + return; + } + data.files = files; + jqXHR = that._onSend(null, data); + jqXHR.then( + function (result, textStatus, jqXHR) { + dfd.resolve(result, textStatus, jqXHR); + }, + function (jqXHR, textStatus, errorThrown) { + dfd.reject(jqXHR, textStatus, errorThrown); + } + ); + } + ); + return this._enhancePromise(promise); + } + data.files = $.makeArray(data.files); + if (data.files.length) { + return this._onSend(null, data); + } + } + return this._getXHRPromise(false, data && data.context); + } + + }); + +})); diff --git a/apps/files/static/files/jquery.iframe-transport.js b/apps/files/static/files/jquery.iframe-transport.js new file mode 100644 index 0000000..8d25c46 --- /dev/null +++ b/apps/files/static/files/jquery.iframe-transport.js @@ -0,0 +1,224 @@ +/* + * jQuery Iframe Transport Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, require, window, document, JSON */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + // Helper variable to create unique names for the transport iframes: + var counter = 0, + jsonAPI = $, + jsonParse = 'parseJSON'; + + if ('JSON' in window && 'parse' in JSON) { + jsonAPI = JSON; + jsonParse = 'parse'; + } + + // The iframe transport accepts four additional options: + // options.fileInput: a jQuery collection of file input fields + // options.paramName: the parameter name for the file form data, + // overrides the name property of the file input field(s), + // can be a string or an array of strings. + // options.formData: an array of objects with name and value properties, + // equivalent to the return data of .serializeArray(), e.g.: + // [{name: 'a', value: 1}, {name: 'b', value: 2}] + // options.initialIframeSrc: the URL of the initial iframe src, + // by default set to "javascript:false;" + $.ajaxTransport('iframe', function (options) { + if (options.async) { + // javascript:false as initial iframe src + // prevents warning popups on HTTPS in IE6: + /*jshint scripturl: true */ + var initialIframeSrc = options.initialIframeSrc || 'javascript:false;', + /*jshint scripturl: false */ + form, + iframe, + addParamChar; + return { + send: function (_, completeCallback) { + form = $('
    '); + form.attr('accept-charset', options.formAcceptCharset); + addParamChar = /\?/.test(options.url) ? '&' : '?'; + // XDomainRequest only supports GET and POST: + if (options.type === 'DELETE') { + options.url = options.url + addParamChar + '_method=DELETE'; + options.type = 'POST'; + } else if (options.type === 'PUT') { + options.url = options.url + addParamChar + '_method=PUT'; + options.type = 'POST'; + } else if (options.type === 'PATCH') { + options.url = options.url + addParamChar + '_method=PATCH'; + options.type = 'POST'; + } + // IE versions below IE8 cannot set the name property of + // elements that have already been added to the DOM, + // so we set the name along with the iframe HTML markup: + counter += 1; + iframe = $( + '' + ).bind('load', function () { + var fileInputClones, + paramNames = $.isArray(options.paramName) ? + options.paramName : [options.paramName]; + iframe + .unbind('load') + .bind('load', function () { + var response; + // Wrap in a try/catch block to catch exceptions thrown + // when trying to access cross-domain iframe contents: + try { + response = iframe.contents(); + // Google Chrome and Firefox do not throw an + // exception when calling iframe.contents() on + // cross-domain requests, so we unify the response: + if (!response.length || !response[0].firstChild) { + throw new Error(); + } + } catch (e) { + response = undefined; + } + // The complete callback returns the + // iframe content document as response object: + completeCallback( + 200, + 'success', + {'iframe': response} + ); + // Fix for IE endless progress bar activity bug + // (happens on form submits to iframe targets): + $('') + .appendTo(form); + window.setTimeout(function () { + // Removing the form in a setTimeout call + // allows Chrome's developer tools to display + // the response result + form.remove(); + }, 0); + }); + form + .prop('target', iframe.prop('name')) + .prop('action', options.url) + .prop('method', options.type); + if (options.formData) { + $.each(options.formData, function (index, field) { + $('') + .prop('name', field.name) + .val(field.value) + .appendTo(form); + }); + } + if (options.fileInput && options.fileInput.length && + options.type === 'POST') { + fileInputClones = options.fileInput.clone(); + // Insert a clone for each file input field: + options.fileInput.after(function (index) { + return fileInputClones[index]; + }); + if (options.paramName) { + options.fileInput.each(function (index) { + $(this).prop( + 'name', + paramNames[index] || options.paramName + ); + }); + } + // Appending the file input fields to the hidden form + // removes them from their original location: + form + .append(options.fileInput) + .prop('enctype', 'multipart/form-data') + // enctype must be set as encoding for IE: + .prop('encoding', 'multipart/form-data'); + // Remove the HTML5 form attribute from the input(s): + options.fileInput.removeAttr('form'); + } + form.submit(); + // Insert the file input fields at their original location + // by replacing the clones with the originals: + if (fileInputClones && fileInputClones.length) { + options.fileInput.each(function (index, input) { + var clone = $(fileInputClones[index]); + // Restore the original name and form properties: + $(input) + .prop('name', clone.prop('name')) + .attr('form', clone.attr('form')); + clone.replaceWith(input); + }); + } + }); + form.append(iframe).appendTo(document.body); + }, + abort: function () { + if (iframe) { + // javascript:false as iframe src aborts the request + // and prevents warning popups on HTTPS in IE6. + // concat is used to avoid the "Script URL" JSLint error: + iframe + .unbind('load') + .prop('src', initialIframeSrc); + } + if (form) { + form.remove(); + } + } + }; + } + }); + + // The iframe transport returns the iframe content document as response. + // The following adds converters from iframe to text, json, html, xml + // and script. + // Please note that the Content-Type for JSON responses has to be text/plain + // or text/html, if the browser doesn't include application/json in the + // Accept header, else IE will show a download dialog. + // The Content-Type for XML responses on the other hand has to be always + // application/xml or text/xml, so IE properly parses the XML response. + // See also + // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation + $.ajaxSetup({ + converters: { + 'iframe text': function (iframe) { + return iframe && $(iframe[0].body).text(); + }, + 'iframe json': function (iframe) { + return iframe && jsonAPI[jsonParse]($(iframe[0].body).text()); + }, + 'iframe html': function (iframe) { + return iframe && $(iframe[0].body).html(); + }, + 'iframe xml': function (iframe) { + var xmlDoc = iframe && iframe[0]; + return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc : + $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || + $(xmlDoc.body).html()); + }, + 'iframe script': function (iframe) { + return iframe && $.globalEval($(iframe[0].body).text()); + } + } + }); + +})); diff --git a/apps/files/static/files/jquery.ui.widget.js b/apps/files/static/files/jquery.ui.widget.js new file mode 100644 index 0000000..a4131b2 --- /dev/null +++ b/apps/files/static/files/jquery.ui.widget.js @@ -0,0 +1,748 @@ +/*! jQuery UI - v1.12.1 - 2018-02-10 + * http://jqueryui.com + * Includes: widget.js + * Copyright jQuery Foundation and other contributors; Licensed MIT */ + +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ "jquery" ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { + + $.ui = $.ui || {}; + + var version = $.ui.version = "1.12.1"; + + + /*! + * jQuery UI Widget 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + + //>>label: Widget + //>>group: Core + //>>description: Provides a factory for creating stateful widgets with a common API. + //>>docs: http://api.jqueryui.com/jQuery.widget/ + //>>demos: http://jqueryui.com/widget/ + + + + var widgetUuid = 0; + var widgetSlice = Array.prototype.slice; + + $.cleanData = ( function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) { + try { + + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } + + // Http://bugs.jquery.com/ticket/8235 + } catch ( e ) {} + } + orig( elems ); + }; + } )( $.cleanData ); + + $.widget = function( name, base, prototype ) { + var existingConstructor, constructor, basePrototype; + + // ProxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + var proxiedPrototype = {}; + + var namespace = name.split( "." )[ 0 ]; + name = name.split( "." )[ 1 ]; + var fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + if ( $.isArray( prototype ) ) { + prototype = $.extend.apply( null, [ {} ].concat( prototype ) ); + } + + // Create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + + // Allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // Allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + + // Extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + + // Copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + + // Track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + } ); + + basePrototype = new base(); + + // We need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = ( function() { + function _super() { + return base.prototype[ prop ].apply( this, arguments ); + } + + function _superApply( args ) { + return base.prototype[ prop ].apply( this, args ); + } + + return function() { + var __super = this._super; + var __superApply = this._superApply; + var returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + } )(); + } ); + constructor.prototype = $.widget.extend( basePrototype, { + + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + } ); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // Redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, + child._proto ); + } ); + + // Remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); + + return constructor; + }; + + $.widget.extend = function( target ) { + var input = widgetSlice.call( arguments, 1 ); + var inputIndex = 0; + var inputLength = input.length; + var key; + var value; + + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; + }; + + $.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string"; + var args = widgetSlice.call( arguments, 1 ); + var returnValue = this; + + if ( isMethodCall ) { + + // If this is an empty collection, we need to have the instance method + // return undefined instead of the jQuery instance + if ( !this.length && options === "instance" ) { + returnValue = undefined; + } else { + this.each( function() { + var methodValue; + var instance = $.data( this, fullName ); + + if ( options === "instance" ) { + returnValue = instance; + return false; + } + + if ( !instance ) { + return $.error( "cannot call methods on " + name + + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + + if ( !$.isFunction( instance[ options ] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + + " widget instance" ); + } + + methodValue = instance[ options ].apply( instance, args ); + + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + } ); + } + } else { + + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat( args ) ); + } + + this.each( function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } + } else { + $.data( this, fullName, new object( options, this ) ); + } + } ); + } + + return returnValue; + }; + }; + + $.Widget = function( /* options, element */ ) {}; + $.Widget._childConstructors = []; + + $.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
    ", + + options: { + classes: {}, + disabled: false, + + // Callbacks + create: null + }, + + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = widgetUuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + this.classesElementLookup = {}; + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + } ); + this.document = $( element.style ? + + // Element within the document + element.ownerDocument : + + // Element is window or document + element.document || element ); + this.window = $( this.document[ 0 ].defaultView || this.document[ 0 ].parentWindow ); + } + + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this._create(); + + if ( this.options.disabled ) { + this._setOptionDisabled( this.options.disabled ); + } + + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + + _getCreateOptions: function() { + return {}; + }, + + _getCreateEventData: $.noop, + + _create: $.noop, + + _init: $.noop, + + destroy: function() { + var that = this; + + this._destroy(); + $.each( this.classesElementLookup, function( key, value ) { + that._removeClass( value, key ); + } ); + + // We can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .off( this.eventNamespace ) + .removeData( this.widgetFullName ); + this.widget() + .off( this.eventNamespace ) + .removeAttr( "aria-disabled" ); + + // Clean up events and states + this.bindings.off( this.eventNamespace ); + }, + + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key; + var parts; + var curOption; + var i; + + if ( arguments.length === 0 ) { + + // Don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + + // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( arguments.length === 1 ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( arguments.length === 1 ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + + _setOption: function( key, value ) { + if ( key === "classes" ) { + this._setOptionClasses( value ); + } + + this.options[ key ] = value; + + if ( key === "disabled" ) { + this._setOptionDisabled( value ); + } + + return this; + }, + + _setOptionClasses: function( value ) { + var classKey, elements, currentElements; + + for ( classKey in value ) { + currentElements = this.classesElementLookup[ classKey ]; + if ( value[ classKey ] === this.options.classes[ classKey ] || + !currentElements || + !currentElements.length ) { + continue; + } + + // We are doing this to create a new jQuery object because the _removeClass() call + // on the next line is going to destroy the reference to the current elements being + // tracked. We need to save a copy of this collection so that we can add the new classes + // below. + elements = $( currentElements.get() ); + this._removeClass( currentElements, classKey ); + + // We don't use _addClass() here, because that uses this.options.classes + // for generating the string of classes. We want to use the value passed in from + // _setOption(), this is the new value of the classes option which was passed to + // _setOption(). We pass this value directly to _classes(). + elements.addClass( this._classes( { + element: elements, + keys: classKey, + classes: value, + add: true + } ) ); + } + }, + + _setOptionDisabled: function( value ) { + this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this._removeClass( this.hoverable, null, "ui-state-hover" ); + this._removeClass( this.focusable, null, "ui-state-focus" ); + } + }, + + enable: function() { + return this._setOptions( { disabled: false } ); + }, + + disable: function() { + return this._setOptions( { disabled: true } ); + }, + + _classes: function( options ) { + var full = []; + var that = this; + + options = $.extend( { + element: this.element, + classes: this.options.classes || {} + }, options ); + + function processClassString( classes, checkOption ) { + var current, i; + for ( i = 0; i < classes.length; i++ ) { + current = that.classesElementLookup[ classes[ i ] ] || $(); + if ( options.add ) { + current = $( $.unique( current.get().concat( options.element.get() ) ) ); + } else { + current = $( current.not( options.element ).get() ); + } + that.classesElementLookup[ classes[ i ] ] = current; + full.push( classes[ i ] ); + if ( checkOption && options.classes[ classes[ i ] ] ) { + full.push( options.classes[ classes[ i ] ] ); + } + } + } + + this._on( options.element, { + "remove": "_untrackClassesElement" + } ); + + if ( options.keys ) { + processClassString( options.keys.match( /\S+/g ) || [], true ); + } + if ( options.extra ) { + processClassString( options.extra.match( /\S+/g ) || [] ); + } + + return full.join( " " ); + }, + + _untrackClassesElement: function( event ) { + var that = this; + $.each( that.classesElementLookup, function( key, value ) { + if ( $.inArray( event.target, value ) !== -1 ) { + that.classesElementLookup[ key ] = $( value.not( event.target ).get() ); + } + } ); + }, + + _removeClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, false ); + }, + + _addClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, true ); + }, + + _toggleClass: function( element, keys, extra, add ) { + add = ( typeof add === "boolean" ) ? add : extra; + var shift = ( typeof element === "string" || element === null ), + options = { + extra: shift ? keys : extra, + keys: shift ? element : keys, + element: shift ? this.element : element, + add: add + }; + options.element.toggleClass( this._classes( options ), add ); + return this; + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement; + var instance = this; + + // No suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // No element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + + // Allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // Copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^([\w:-]*)\s*(.*)$/ ); + var eventName = match[ 1 ] + instance.eventNamespace; + var selector = match[ 2 ]; + + if ( selector ) { + delegateElement.on( eventName, selector, handlerProxy ); + } else { + element.on( eventName, handlerProxy ); + } + } ); + }, + + _off: function( element, eventName ) { + eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) + + this.eventNamespace; + element.off( eventName ).off( eventName ); + + // Clear the stack to avoid memory leaks (#10056) + this.bindings = $( this.bindings.not( element ).get() ); + this.focusable = $( this.focusable.not( element ).get() ); + this.hoverable = $( this.hoverable.not( element ).get() ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + this._addClass( $( event.currentTarget ), null, "ui-state-hover" ); + }, + mouseleave: function( event ) { + this._removeClass( $( event.currentTarget ), null, "ui-state-hover" ); + } + } ); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + this._addClass( $( event.currentTarget ), null, "ui-state-focus" ); + }, + focusout: function( event ) { + this._removeClass( $( event.currentTarget ), null, "ui-state-focus" ); + } + } ); + }, + + _trigger: function( type, event, data ) { + var prop, orig; + var callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + + // The original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // Copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } + }; + + $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + + var hasOptions; + var effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + + if ( options.delay ) { + element.delay( options.delay ); + } + + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue( function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + } ); + } + }; + } ); + + var widget = $.widget; + + + + +})); diff --git a/apps/files/static/files/upload.js b/apps/files/static/files/upload.js new file mode 100644 index 0000000..13e5ebc --- /dev/null +++ b/apps/files/static/files/upload.js @@ -0,0 +1,199 @@ +class MultipleFilesWidget { + constructor(div) { + this.root = div + this.fieldName = this.root.getAttribute('data-field') + this.fileList = [] + this.element = { + list: this.root.querySelector('.filewidget--list'), + uploadButton: this.root.querySelector('.filewidget--input'), + field: document.querySelector(`[name="${this.fieldName}"]`) + } + + const self = this + + // Set up jquery-fileupload + $(this.element.uploadButton).fileupload({ + dataType: 'json', + paramName: 'file', + done: function (e, data) { + // process server response + if (!data.result.is_valid) { + throw new Error('Server sent us invalid data!') + } + + self.fileFinished.bind(self)(data.result) + }, + add: function (e, data) { + $.each(data.files, (index, file) => { + self.addFile.bind(self)(null, file.name, self.setFileInProgress.bind(self)) + }); + + data.process().done(function () { + data.submit() + }) + } + }) + + // Set up listening for restore + this.element.field.addEventListener('change', evt => { + let idList = evt.srcElement.value.split(",") + for (let id of idList) { + this.addFile(id, id, this.setFileDone.bind(this)) + } + this.viewFileList() + }) + } + + addFile(id, name, stateFunc) { + console.log('addfile here!') + + let li = document.createElement('li') + li.className = 'filewidget--file' + li.innerHTML = + ` + ` + + let file = { + root: li, + element: { + icon: li.querySelector('.filewidget--file--icon'), + name: li.querySelector('.filewidget--file--name'), + actions: li.querySelector('.filewidget--file--actions'), + }, + name: name, + id: id || null, + } + + stateFunc(file) + + this.fileList.push(file) + this.viewFileList() + } + + removeFile(file) { + // Remove file from display + this.element.list.removeChild(file.root) + + // Remove file from fileList + this.fileList = this.fileList.filter(cmpFile => cmpFile === file) + } + + fileFinished(serverResponse) { + // Using data, find the file + let file = this.fileList.filter(file => file.name === serverResponse.name)[0] + + // Set the ID + file.id = serverResponse.id + + // Set the file state now it's finished + this.setFileDone(file) + this.viewFileList() + + // Do this last tho + this.updateFormField() + } + + // + // Update the field that keeps track of file IDs + // + + updateFormField() { + console.log("updateFormField", this) + + let oldVal = this.element.field.value + this.element.field.value = this.fileList.filter(f => f.id != null) + .map(f => f.id) + .toString() + + // Mark form as dirty + if (this.element.field.value !== oldVal) { + document.getElementById('case-study-form').dispatchEvent(new Event('dirty')) + } + } + + // + // Manage the state of individual files in the widget + // + + setFileState(file, state) { + file.element.icon.classList = `filewidget--file--icon ${state.iconClassList}` + file.element.name.innerText = state.fileName + file.element.actions.innerHTML = state.actions || '' + } + + setFileInProgress(file) { + this.setFileState(file, { + iconClassList: 'fa fa-spinner fa-spin', + fileName: `${file.name} (uploading...)` + }) + } + + setFileDeletingInProgress(file) { + this.setFileState(file, { + iconClassList: 'fa fa-spinner fa-spin', + fileName: `Deleting...` + }) + } + + setFileDeletingFailed(file) { + this.setFileState(file, { + iconClassList: 'fa fa-exclamation-triangle', + fileName: `${file.name} (delete failed!)` + }) + } + + setFileDone(file) { + console.log("setFileDone", file) + + this.setFileState(file, { + iconClassList: 'fa fa-file-o', + fileName: `${file.name}`, + actions: '' + }) + + file.element.actions.querySelector('a').addEventListener('click', () => { + this.setFileDeletingInProgress(file) + + return fetch(`/files/delete/${file.id}/`, { + method: 'POST', + credentials: "same-origin", + headers: { + "X-CSRFToken": jQuery("[name=csrfmiddlewaretoken]").val(), + } + }).then(response => { + if (response.ok) { + this.removeFile(file) + } else { + this.setFileDeletingFailed(file) + } + }) + }) + } + + // + // Redraw the file list + // + + viewFileList() { + console.log("viewFileList", this) + + for (let file of this.fileList) { + // Check if it's appended to the list + if (!this.element.list.contains(file.root)) { + this.element.list.appendChild(file.root) + } + } + } +} + +$(function() { + window.official_project_documents = new MultipleFilesWidget( + document.querySelector('[data-field=official_project_documents_files]') + ) + window.other_documents = new MultipleFilesWidget( + document.querySelector('[data-field=other_documents_files]') + ) + window.shapefiles = new MultipleFilesWidget( + document.querySelector('[data-field=shapefiles_files]') + ) +}) diff --git a/apps/files/tests.py b/apps/files/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/files/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/files/urls.py b/apps/files/urls.py new file mode 100644 index 0000000..efae6c9 --- /dev/null +++ b/apps/files/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url + +from .views import FileUploadView, FileDeleteView + +app_name = 'files' + +urlpatterns = [ + url(r'^upload/$', FileUploadView.as_view(), name='upload'), + url(r'^delete/(?P\d+)/$', FileDeleteView.as_view(), name='delete'), +] diff --git a/apps/files/views.py b/apps/files/views.py new file mode 100644 index 0000000..b1b38b0 --- /dev/null +++ b/apps/files/views.py @@ -0,0 +1,45 @@ +from django.core.exceptions import PermissionDenied +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import JsonResponse +from django.shortcuts import render +from django.views.generic import FormView, DetailView + +from .forms import FileForm +from .models import File + +class FileUploadView(LoginRequiredMixin, FormView): + model = File + form_class = FileForm + + def form_valid(self, form): + self.object = form.save(commit=False) + self.object.user = self.request.user + self.object.save() + + return JsonResponse({ + 'is_valid': True, 'url': self.object.file.url, + 'name': self.object.file.name, + 'id': self.object.pk + }) + + def form_invalid(self, form): + return JsonResponse({'is_valid': False, 'errors': form.errors}) + + +class FileDeleteView(LoginRequiredMixin, DetailView): + model = File + + def get(self, request, *args, **kwargs): + return self.post(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + + if request.user != self.object.user: + raise PermissionDenied + + self.object.delete() + + return JsonResponse({ + 'success': True + }) diff --git a/apps/map/forms.py b/apps/map/forms.py index 7904303..0338948 100644 --- a/apps/map/forms.py +++ b/apps/map/forms.py @@ -1,5 +1,5 @@ -from django.urls import reverse from django import forms +from django.urls import reverse, reverse_lazy from django.utils.translation import ugettext as _ from django.utils.safestring import mark_safe @@ -9,7 +9,10 @@ from crispy_forms.bootstrap import Tab, TabHolder, PrependedText, FormActions from dal import autocomplete from leaflet.forms.widgets import LeafletWidget +from apps.files.models import File + from .models import CaseStudy, SpatialRefSys +from .widgets import CommaSeparatedTextInput class MinimumZoomWidget(LeafletWidget): @@ -18,6 +21,7 @@ class MinimumZoomWidget(LeafletWidget): class BaseCaseStudyForm(forms.models.ModelForm): """Base form class for the CaseStudy model.""" + def __init__(self, *args, **kwargs): super(BaseCaseStudyForm, self).__init__(*args, **kwargs) self.helper = FormHelper(self) @@ -27,6 +31,7 @@ class BaseCaseStudyForm(forms.models.ModelForm): self.helper.form_action = 'add' self.helper.label_class = 'col-lg-2' self.helper.field_class = 'col-lg-8' + self.helper.include_media = False class Meta: model = CaseStudy @@ -37,9 +42,6 @@ class BaseCaseStudyForm(forms.models.ModelForm): 'SCALE': False } }), - 'official_project_documents': forms.ClearableFileInput(attrs={'multiple': True}), - 'other_documents': forms.ClearableFileInput(attrs={'multiple': True}), - 'shapefiles': forms.ClearableFileInput(attrs={'multiple': True}), } @@ -79,9 +81,52 @@ class ShortCaseStudyForm(BaseCaseStudyForm): ] +class BootstrapClearableFileInput(forms.ClearableFileInput): + template_name = 'map/forms/widgets/file.html' + + class LongCaseStudyForm(BaseCaseStudyForm): """Long version of the CaseStudy form.""" + official_project_documents = forms.FileField( + widget=BootstrapClearableFileInput(attrs={ + 'url': reverse_lazy('files:upload'), + 'field': 'official_project_documents_files', + }), required=False + ) + + official_project_documents_files = forms.ModelMultipleChoiceField( + queryset=File.objects.all(), + widget=CommaSeparatedTextInput(), + required=False + ) + + other_documents = forms.FileField( + widget=BootstrapClearableFileInput(attrs={ + 'url': reverse_lazy('files:upload'), + 'field': 'other_documents_files', + }), required=False + ) + + other_documents_files = forms.ModelMultipleChoiceField( + queryset=File.objects.all(), + widget=CommaSeparatedTextInput(), + required=False + ) + + shapefiles = forms.FileField( + widget=BootstrapClearableFileInput(attrs={ + 'url': reverse_lazy('files:upload'), + 'field': 'shapefiles_files', + }), required=False + ) + + shapefiles_files = forms.ModelMultipleChoiceField( + queryset=File.objects.all(), + widget=CommaSeparatedTextInput(), + required=False + ) + coordinate_reference_system = forms.ModelChoiceField( queryset=SpatialRefSys.objects.all(), widget=autocomplete.ModelSelect2(url='srs-autocomplete'), @@ -126,7 +171,7 @@ class LongCaseStudyForm(BaseCaseStudyForm): POSITIVE_CASE_TYPE_CHOICES = [ (choice[0], mark_safe('%s
    %s' % (choice[1], self.POSITIVE_CASE_TYPE_HELP[choice[0]]))) - for choice in CaseStudy.POSITIVE_CASE_TYPE_CHOICES + for choice in CaseStudy.POSITIVE_CASE_TYPE_CHOICES ] self.fields['positive_case_type'] = forms.ChoiceField( @@ -275,8 +320,11 @@ class LongCaseStudyForm(BaseCaseStudyForm): Tab( _("Uploads"), 'official_project_documents', + 'official_project_documents_files', 'other_documents', + 'other_documents_files', 'shapefiles', + 'shapefiles_files', 'coordinate_reference_system', 'name_of_territory_or_area', 'shown_on_other_platforms', @@ -289,3 +337,11 @@ class LongCaseStudyForm(BaseCaseStudyForm): class Meta(BaseCaseStudyForm.Meta): exclude = ('approved',) + + class Media: + js = ( + 'files/jquery.ui.widget.js', + 'files/jquery.iframe-transport.js', + 'files/jquery.fileupload.js', + 'files/upload.js', + ) diff --git a/apps/map/migrations/0056_delete_shapefile.py b/apps/map/migrations/0056_delete_shapefile.py new file mode 100644 index 0000000..99f4ddd --- /dev/null +++ b/apps/map/migrations/0056_delete_shapefile.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-04-19 17:32 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('map', '0055_auto_20180419_1650'), + ] + + operations = [ + migrations.DeleteModel( + name='Shapefile', + ), + ] diff --git a/apps/map/migrations/0057_auto_20180423_0220.py b/apps/map/migrations/0057_auto_20180423_0220.py new file mode 100644 index 0000000..f2fdcf2 --- /dev/null +++ b/apps/map/migrations/0057_auto_20180423_0220.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-04-23 02:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('files', '0001_initial'), + ('map', '0056_delete_shapefile'), + ] + + operations = [ + migrations.RemoveField( + model_name='casestudy', + name='official_project_documents', + ), + migrations.AddField( + model_name='casestudy', + name='official_project_documents', + field=models.ManyToManyField(blank=True, help_text='Attach any legal or official documents that relate to the project.', null=True, related_name='official_project_document_for', to='files.File', verbose_name='Official project documents'), + ), + migrations.RemoveField( + model_name='casestudy', + name='other_documents', + ), + migrations.AddField( + model_name='casestudy', + name='other_documents', + field=models.ManyToManyField(blank=True, help_text='Attach any other documents that relate to the project.', null=True, related_name='other_document_for', to='files.File', verbose_name='Other documents'), + ), + migrations.RemoveField( + model_name='casestudy', + name='shapefiles', + ), + migrations.AddField( + model_name='casestudy', + name='shapefiles', + field=models.ManyToManyField(blank=True, help_text='If you have territory that you would like to show in relation to this project - e.g. Bienes Comunales de Ixtepec etc. This is a set of 3 or more (often 5-6) files with file extensions like .cpg, .dbf, .prj, .qpj, .shp, .shx', null=True, related_name='shapefile_for', to='files.File', verbose_name='Shapefiles'), + ), + ] diff --git a/apps/map/migrations/0058_auto_20180429_2205.py b/apps/map/migrations/0058_auto_20180429_2205.py new file mode 100644 index 0000000..c85401b --- /dev/null +++ b/apps/map/migrations/0058_auto_20180429_2205.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-04-29 22:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('map', '0057_auto_20180423_0220'), + ] + + operations = [ + migrations.AlterField( + model_name='casestudy', + name='official_project_documents', + field=models.ManyToManyField(blank=True, help_text='Attach any legal or official documents that relate to the project.', related_name='official_project_document_for', to='files.File', verbose_name='Official project documents'), + ), + migrations.AlterField( + model_name='casestudy', + name='other_documents', + field=models.ManyToManyField(blank=True, help_text='Attach any other documents that relate to the project.', related_name='other_document_for', to='files.File', verbose_name='Other documents'), + ), + migrations.AlterField( + model_name='casestudy', + name='shapefiles', + field=models.ManyToManyField(blank=True, help_text='If you have territory that you would like to show in relation to this project - e.g. Bienes Comunales de Ixtepec etc. This is a set of 3 or more (often 5-6) files with file extensions like .cpg, .dbf, .prj, .qpj, .shp, .shx', related_name='shapefile_for', to='files.File', verbose_name='Shapefiles'), + ), + ] diff --git a/apps/map/models.py b/apps/map/models.py index 3f63e60..aa0d1ec 100644 --- a/apps/map/models.py +++ b/apps/map/models.py @@ -17,8 +17,7 @@ from . import validators class CaseStudyDraft(models.Model): author = models.ForeignKey( - User, - on_delete=models.CASCADE + User, on_delete=models.CASCADE ) data = models.TextField() @@ -36,12 +35,6 @@ class SpatialRefSys(connection.ops.spatial_ref_sys()): verbose_name = "spatial reference system" -class Shapefile(models.Model): - file = models.FileField( - upload_to='shapefiles/', - ) - - class CaseStudyQuerySet(models.QuerySet): def approved(self): return self.filter( @@ -1007,31 +1000,31 @@ class CaseStudy(models.Model): ## # 4.1 - official_project_documents = models.FileField( + official_project_documents = models.ManyToManyField( + 'files.File', + related_name='official_project_document_for', verbose_name=_("Official project documents"), help_text=_("Attach any legal or official documents that relate to the project."), - default=None, - null=True, blank=True, ) # 4.2 - other_documents = models.FileField( + other_documents = models.ManyToManyField( + 'files.File', + related_name='other_document_for', verbose_name=_("Other documents"), help_text=_("Attach any other documents that relate to the project."), - default=None, - null=True, blank=True, ) # 4.3.1 - shapefiles = models.FileField( + shapefiles = models.ManyToManyField( + 'files.File', + related_name='shapefile_for', verbose_name=_("Shapefiles"), help_text=_("If you have territory that you would like to show in relation to this project - e.g. Bienes \ Comunales de Ixtepec etc. This is a set of 3 or more (often 5-6) files with file extensions like \ .cpg, .dbf, .prj, .qpj, .shp, .shx"), - default=None, - null=True, blank=True ) @@ -1084,7 +1077,6 @@ class CaseStudy(models.Model): # Continue normal save method by calling original save method. super(CaseStudy, self).save(*args, **kwargs) - def is_video_youtube(self): return self.video.count("youtube.com") > 0 @@ -1092,7 +1084,6 @@ class CaseStudy(models.Model): """Gets the 11 character YouTube video ID from the video field.""" return parse.parse_qs(parse.urlparse(self.video).query)["v"][0] - def is_video_vimeo(self): return self.video.count("vimeo.com") > 0 @@ -1100,7 +1091,6 @@ class CaseStudy(models.Model): """Gets the 11 number video ID from the video field.""" return parse.urlparse(self.video).path - def get_negative_case_reasons_no_other(self): """Return a list of negative case reasons, minus the 'other' choice (if selected)""" choices = self.get_negative_case_reasons_list() diff --git a/apps/map/templates/map/form.html b/apps/map/templates/map/form.html index 1f9c3ce..f2d1ddd 100644 --- a/apps/map/templates/map/form.html +++ b/apps/map/templates/map/form.html @@ -33,6 +33,41 @@ border: 1px solid #ccc; font-weight: bold; } + + .filewidget--input { position: absolute; left: -1000px; } + .filewidget--list { + list-style-type: none; + padding-left: 0; + margin-left: 0; + margin-top: 12px; + } + + .filewidget--file { + border: 1px solid #aaa; + display: inline-block; + padding: 4px 8px; + width: 34em; + background-color: #eee; + border-radius: 4px; + cursor: default; + } + + .filewidget--file--icon { + margin-right: 8px; + } + + .filewidget--file--actions { + float: right; + } + + .filewidget--file--actions a { + color: black; + cursor: pointer; + } + + .filewidget--file--actions a:hover { + color: red; + } {% endblock %} @@ -509,10 +544,10 @@ function initDrafts() { var button = new SaveButton(document.querySelector('.savebutton')) // Dirty forms set up - $('#case-study-form').dirtyForms() + $('#case-study-form').dirtyForms(); $('#case-study-form').on('dirty.dirtyforms', ev => { button.switchStateUnsaved() - }) + }); // Save button button.element.button.addEventListener('click', evt => { diff --git a/apps/map/templates/map/forms/widgets/file.html b/apps/map/templates/map/forms/widgets/file.html new file mode 100644 index 0000000..d17a136 --- /dev/null +++ b/apps/map/templates/map/forms/widgets/file.html @@ -0,0 +1,7 @@ +
    + +
      +
      diff --git a/apps/map/views.py b/apps/map/views.py index 005c374..7d7f525 100644 --- a/apps/map/views.py +++ b/apps/map/views.py @@ -1,3 +1,5 @@ +import json + from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.core.mail import send_mail @@ -11,6 +13,8 @@ from django.views.generic.edit import CreateView from dal import autocomplete +from apps.files.models import File + from .models import CaseStudy, CaseStudyDraft, SpatialRefSys from .forms import ShortCaseStudyForm, LongCaseStudyForm @@ -56,11 +60,26 @@ class BaseForm(LoginRequiredMixin, CreateView): ) def form_valid(self, form): + form.cleaned_data.pop('official_project_documents', None) + form.cleaned_data.pop('other_documents', None) + form.cleaned_data.pop('shapefiles', None) + self.object = form.save() + + self.object.official_project_documents = form.cleaned_data.get( + 'official_project_document_files', [] + ) + self.object.other_documents = form.cleaned_data.get( + 'other_documents_files', [] + ) + self.object.shapefiles = form.cleaned_data.get( + 'shapefiles_files', [] + ) + self.send_email() # Delete the corresponding draft - draft = CaseStudyDraft.objects.get(author=request.user) + draft = CaseStudyDraft.objects.get(author=self.request.user) if draft: draft.delete() @@ -100,7 +119,7 @@ class SpatialRefSysAutocomplete(autocomplete.Select2QuerySetView): return qs -class Drafts(View): +class Drafts(LoginRequiredMixin, View): """Retrieve or save a draft.""" def get_object(self, request): @@ -110,9 +129,6 @@ class Drafts(View): return None def get(self, request): - if not request.user.is_authenticated: - return HttpResponse(status=403) # Forbidden - draft = self.get_object(request) if draft == None: return HttpResponse(status=404) # Not Found @@ -120,9 +136,6 @@ class Drafts(View): return HttpResponse(draft.data, content_type="application/json") def put(self, request): - if not request.user.is_authenticated: - return HttpResponse(status=403) # Forbidden - # Find an existing object is there is one draft = self.get_object(request) if draft == None: @@ -136,10 +149,35 @@ class Drafts(View): return HttpResponse(status=200) # OK def delete(self, request): - if not request.user.is_authenticated: - return HttpResponse(status=403) # Forbidden - draft = self.get_object(request) + if draft != None: + data = json.loads(draft.data) + + for k in ['official_project_documents', 'other_documents', + 'shapefiles']: + + items = list(filter( + lambda x: ( + x['name'] == '{0}_files'.format(k) + and x['value'] != '' + ), data['data']['form'] + )) + + try: + items = items[0]['value'].split(',') + except IndexError: + continue + + for item in items: + try: + f = File.objects.get(id=item) + if f.user != self.request.user: + continue + f.delete() + except File.DoesNotExist: + continue + draft.delete() + return HttpResponse(status=204) diff --git a/apps/map/widgets.py b/apps/map/widgets.py new file mode 100644 index 0000000..4259571 --- /dev/null +++ b/apps/map/widgets.py @@ -0,0 +1,21 @@ +from django.forms import widgets + + +class CommaSeparatedTextInput(widgets.HiddenInput): + def format_value(self, value): + try: + value = ','.join(value) + except TypeError: + value = '' + return super().format_value(value) + + def value_from_datadict(self, data, files, name): + value = super().value_from_datadict(data, files, name) + + if value == '': + return None + + try: + return value.split(',') + except AttributeError: + return None diff --git a/ojusomap/settings.py b/ojusomap/settings.py index b8ca7f5..a12c158 100644 --- a/ojusomap/settings.py +++ b/ojusomap/settings.py @@ -40,6 +40,7 @@ else: INSTALLED_APPS = [ 'apps.contact', + 'apps.files', 'apps.map', 'apps.profiles', 'avatar', diff --git a/ojusomap/templates/base.html b/ojusomap/templates/base.html index 8c79a51..8492519 100644 --- a/ojusomap/templates/base.html +++ b/ojusomap/templates/base.html @@ -54,87 +54,87 @@ } {% endblock %} - - - + + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + + {% block content %} + {% endblock %} + + {% block footer %} + - {% endblock %} +
      + + {% endblock %} -{# CDN Javascript #} - - -{% block scripts %}{% endblock %} + {# CDN Javascript #} + + + {% block scripts %}{% endblock %} {% endspaceless %} diff --git a/ojusomap/templates/base_page.html b/ojusomap/templates/base_page.html index 02ec3bf..99b138a 100644 --- a/ojusomap/templates/base_page.html +++ b/ojusomap/templates/base_page.html @@ -1,14 +1,14 @@ {% extends "base.html" %} {% load i18n %} -{% block stylesheets %} - {{ block.super }} - +{% block stylesheets %} +{{ block.super }} + {% endblock %} {% block content %} @@ -16,12 +16,12 @@
      -

      {% block title %}{% endblock %}

      -

      {% block description %}{% endblock %}

      +

      {% block title %}{% endblock %}

      +

      {% block description %}{% endblock %}

      - {% block inner %} - {% endblock %} + {% block inner %} + {% endblock %}
      {% endblock %} diff --git a/ojusomap/urls.py b/ojusomap/urls.py index 4abb649..4b32c6a 100644 --- a/ojusomap/urls.py +++ b/ojusomap/urls.py @@ -68,12 +68,12 @@ urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^avatar/', include('avatar.urls')), url(r'^cas/', include('cas_server.urls', namespace='cas_server')), + url(r'^files/', include('apps.files.urls')), # url(r'^contact/', include('apps.contact.urls'), name="contact"), ] urlpatterns += i18n_patterns( url(r'^accounts/profile/', include('apps.profiles.urls', namespace="profile")), - # url(r'^accounts/logout/?$', include('django.contrib.auth.views.logout'), {'next_page': '/'}), url(r'^accounts/', include('registration.backends.default.urls')), url(r'', include('apps.map.urls'), name="map"), ) diff --git a/requirements-devel.txt b/requirements-devel.txt new file mode 100644 index 0000000..fb54305 --- /dev/null +++ b/requirements-devel.txt @@ -0,0 +1,5 @@ +-r requirements.txt + +bpython==0.17.1 +isort==4.3.4 +prospector==0.12.7