Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
=============
django-s3file
=============

Expand All @@ -11,19 +12,23 @@ limit.

|PyPi Version| |Build Status| |Test Coverage| |GitHub license|

--------
Features
--------

- lightweight: less 200 lines
- no JavaScript or Python dependencies (no jQuery)
- easy integration
- works just like the built-in
- extendable JavaScript API

-------------
For the Nerds
-------------

.. image:: http-message-flow.svg

------------
Installation
------------

Expand All @@ -34,6 +39,8 @@ Just install S3file using ``pip``.
.. code:: bash

pip install django-s3file
# or
pipenv install django-s3file

Add the S3File app and middleware in your settings:

Expand All @@ -52,6 +59,7 @@ Add the S3File app and middleware in your settings:
'...',
)

-----
Usage
-----

Expand All @@ -63,7 +71,7 @@ when the ``DEFAULT_FILE_STORAGE`` setting is set to
``django-storages``\ ’ ``S3Boto3Storage``.

Setting up the AWS S3 bucket
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
----------------------------

Upload folder
~~~~~~~~~~~~~
Expand All @@ -89,14 +97,62 @@ to your CORS policy.
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>GET</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

Progress Bar
------------

S3File does emit progress signals that can be used to display some kind of progress bar.
Signals named `progress` are emitted for both each individual file input as well as for
the form as a whole.

The progress signal carries the following details:

.. code:: javascript

console.log(event.detail)

{
progress: 0.4725307607171312 // total upload progress of either a form or single input
loaded: 1048576 // total upload progress of either a form or single input
total: 2219064 // total bytes to upload
currentFile: File {…} // file object
currentFileName: "text.txt" // file name of the file currently uploaded
currentFileProgress: 0.47227834703299176 // upload progress of that file
originalEvent: ProgressEvent {…} // the original XHR onprogress event
}


The following example implements a Boostrap progress bar for upload progress of an
entire form.

.. code:: html

<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
</div>

.. code:: javascript

(function () {
var form = document.getElementsByTagName('form')[0]
var progressBar = document.getElementsByClassName('progress-bar')[0

form.addEventListener('progress', function (event) {
// event.detail.progress is a value between 0 and 1
var percent = Math.round(event.detail.progress * 100)

progressBar.setAttribute('style', 'width:' + percent + '%')
progressBar.setAttribute('aria-valuenow', percent)
progressBar.innerText = percent + '%'
})
})()

Uploading multiple files
~~~~~~~~~~~~~~~~~~~~~~~~
------------------------

Django does have limited support for `uploading multiple files`_. S3File
fully supports this feature. The custom middleware makes ensure that
Expand Down
42 changes: 40 additions & 2 deletions s3file/static/s3file/js/s3file.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
}
}

function request (method, url, data) {
function request (method, url, data, fileInput, file, form) {
file.loaded = 0
return new Promise(function (resolve, reject) {
var xhr = new window.XMLHttpRequest()
xhr.open(method, url)
Expand All @@ -39,16 +40,51 @@
}
}

xhr.upload.onprogress = function (e) {
var diff = e.loaded - file.loaded
form.loaded += diff
fileInput.loaded += diff
file.loaded = e.loaded

var defaultEventData = {
currentFile: file,
currentFileName: file.name,
currentFileProgress: Math.min(e.loaded / e.total, 1),
originalEvent: e
}

form.dispatchEvent(new window.CustomEvent('progress', {
detail: Object.assign({
progress: Math.min(form.loaded / form.total, 1),
loaded: form.loaded,
total: form.total
}, defaultEventData)
}))

fileInput.dispatchEvent(new window.CustomEvent('progress', {
detail: Object.assign({
progress: Math.min(fileInput.loaded / fileInput.total, 1),
loaded: fileInput.loaded,
total: fileInput.total
}, defaultEventData)
}))
}

xhr.onerror = function () {
reject(xhr.statusText)
}

xhr.send(data)
})
}

function uploadFiles (form, fileInput, name) {
var url = fileInput.getAttribute('data-url')
fileInput.loaded = 0
fileInput.total = 0
var promises = Array.from(fileInput.files).map(function (file) {
form.total += file.size
fileInput.total += file.size
var s3Form = new window.FormData()
Array.from(fileInput.attributes).forEach(function (attr) {
var name = attr.name
Expand All @@ -61,7 +97,7 @@
s3Form.append('success_action_status', '201')
s3Form.append('Content-Type', file.type)
s3Form.append('file', file)
return request('POST', url, s3Form)
return request('POST', url, s3Form, fileInput, file, form)
})
Promise.all(promises).then(function (results) {
results.forEach(function (result) {
Expand Down Expand Up @@ -93,6 +129,8 @@

function uploadS3Inputs (form) {
window.uploading = 0
form.loaded = 0
form.total = 0
var inputs = form.querySelectorAll('.s3file')
Array.from(inputs).forEach(function (input) {
window.uploading += 1
Expand Down
29 changes: 21 additions & 8 deletions tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from contextlib import contextmanager

import pytest
Expand All @@ -7,7 +8,6 @@
from selenium.webdriver.support.expected_conditions import staleness_of
from selenium.webdriver.support.wait import WebDriverWait

from s3file.forms import S3FileInputMixin
from tests.testapp.forms import UploadForm

try:
Expand All @@ -31,13 +31,6 @@ class TestS3FileInput:
def url(self):
return reverse('upload')

@pytest.fixture(autouse=True)
def patch(self):
if S3FileInputMixin not in ClearableFileInput.__bases__:
ClearableFileInput.__bases__ = \
(S3FileInputMixin,) + ClearableFileInput.__bases__
pass

@pytest.fixture
def freeze(self, monkeypatch):
"""Freeze datetime and UUID."""
Expand Down Expand Up @@ -171,6 +164,26 @@ def test_file_insert_submit_value(self, driver, live_server, upload_file, freeze
assert 'save_continue' in driver.page_source
assert 'continue_value' in driver.page_source

def test_progress(self, driver, live_server, upload_file, freeze):
driver.get(live_server + self.url)
file_input = driver.find_element_by_xpath('//input[@type=\'file\']')
file_input.send_keys(upload_file)
assert file_input.get_attribute('name') == 'file'
save_button = driver.find_element_by_xpath('//input[@name=\'save\']')
with wait_for_page_load(driver, timeout=10):
save_button.click()
assert 'save' in driver.page_source

driver.get(live_server + self.url)
file_input = driver.find_element_by_xpath('//input[@type=\'file\']')
file_input.send_keys(upload_file)
assert file_input.get_attribute('name') == 'file'
save_button = driver.find_element_by_xpath('//button[@name=\'save_continue\']')
with wait_for_page_load(driver, timeout=10):
save_button.click()
response = json.loads(driver.find_elements_by_css_selector('pre')[0].text)
assert response['progress'] == '1'

def test_media(self):
assert ClearableFileInput().media._js == ['s3file/js/s3file.js']

Expand Down
9 changes: 9 additions & 0 deletions tests/testapp/forms.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
from django import forms

from s3file.forms import S3FileInputMixin

from .models import FileModel

if S3FileInputMixin not in forms.ClearableFileInput.__bases__:
forms.ClearableFileInput.__bases__ = \
(S3FileInputMixin,) + forms.ClearableFileInput.__bases__


class UploadForm(forms.ModelForm):
class Meta:
model = FileModel
fields = ('file',)
widgets = {
'file': forms.ClearableFileInput(attrs={'multiple': True})
}
32 changes: 30 additions & 2 deletions tests/testapp/templates/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
{% load staticfiles %}
<html>
<head>
{#<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">#}
<script type="text/javascript">
window.onerror = function (msg) {
document.getElementById('body').setAttribute("JSError", msg);
document.getElementById('body').setAttribute('JSError', msg)
}
</script>
{{ form.media.css }}
Expand All @@ -15,6 +16,33 @@
{{ form }}
<input type="submit" name="save" value="Save"/>
<button type="submit" name="save_continue" value="continue_value">Save &amp; continue</button>
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0"
aria-valuemax="100">0%
</div>
</div>
</form>
{{ form.media.js }}
</body>
<script>
(function () {
var form = document.getElementsByTagName('form')[0]
var progressBar = document.getElementsByClassName('progress-bar')[0]

var input = document.createElement('input')
input.type = 'hidden'
input.name = 'progress'
form.appendChild(input)

form.addEventListener('progress', function (event) {
// event.detail.progress is a value between 0 and 1
var percent = Math.round(event.detail.progress * 100)

progressBar.setAttribute('style', 'width:' + percent + '%')
progressBar.setAttribute('aria-valuenow', percent)
progressBar.innerText = percent + '%'

input.value = event.detail.progress
})
})()
</script>
</body>