Skip to content

Monkey patch ClearableFileInput #62

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 9, 2017
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
6 changes: 1 addition & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ dist: trusty
cache:
- apt
- pip
services:
- redis
addons:
firefox: latest
apt:
Expand All @@ -14,13 +12,11 @@ addons:
packages:
- google-chrome-stable
python:
- '2.7'
- '3.5'
- '3.6'
env:
global:
- GECKO_DRIVER_VERSION=v0.16.1
- CHROME_DRIVER_VERSION=2.29
- CHROME_DRIVER_VERSION=2.32
matrix:
- DJANGO=18
- DJANGO=110
Expand Down
38 changes: 8 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

A lightweight file upload input for Django and Amazon S3.

Django-S3File allows you to upload files directly AWS S3 effectively
bypassing your application server. This allows you to avoid long running
requests from large file uploads.

[![PyPi Version](https://img.shields.io/pypi/v/django-s3file.svg)](https://pypi.python.org/pypi/django-s3file/)
[![Build Status](https://travis-ci.org/codingjoe/django-s3file.svg?branch=master)](https://travis-ci.org/codingjoe/django-s3file)
[![Test Coverage](https://coveralls.io/repos/codingjoe/django-s3file/badge.svg?branch=master)](https://coveralls.io/r/codingjoe/django-s3file)
Expand All @@ -11,8 +15,7 @@ A lightweight file upload input for Django and Amazon S3.

* lightweight: less 200 lines
* no JavaScript or Python dependencies (no jQuery)
* Python 3 and 2 support
* auto enabled based on your environment
* easy integration
* works just like the build-in

## Installation
Expand Down Expand Up @@ -46,37 +49,12 @@ MIDDLEWARE = (

## Usage

By default S3File will replace Django's `FileField` widget,
but you can also specify the widget manually and pass custom attributes.
S3File automatically replaces Django's `ClearableFileInput` widget,
you do not need to alter your code at all.

The `FileField`'s widget is only than automatically replaced when the
The `ClearableFileInput` widget is only than automatically replaced when the
`DEFAULT_FILE_STORAGE` setting is set to `django-storages`' `S3Boto3Storage`.

### Simple integrations

**forms.py**

```python
from django import forms
from django.db import models
from s3file.forms import S3FileInput


class ImageModel(models.Model):
file = models.FileField(upload_to='path/to/files')


class MyModelForm(forms.ModelForm):
class Meta:
model = ImageModel
fields = ('file',)
widgets = {
'file': S3FileInput(attrs={'accept': 'image/*'})
}
```
**Done!** No really, that's all that needs to be done.


### Setting up the AWS S3 bucket

### Upload folder
Expand Down
11 changes: 4 additions & 7 deletions s3file/apps.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
from django.apps import AppConfig

try:
from storages.backends.s3boto3 import S3Boto3Storage
except ImportError:
from storages.backends.s3boto import S3BotoStorage as S3BotoStorage


class S3FileConfig(AppConfig):
name = 's3file'
verbose_name = 'S3File'

def ready(self):
from django.forms import FileField
from django.core.files.storage import default_storage
from storages.backends.s3boto3 import S3Boto3Storage

if isinstance(default_storage, S3Boto3Storage):
from django import forms
from .forms import S3FileInput

FileField.widget = S3FileInput
forms.ClearableFileInput.__new__ = \
lambda cls, *args, **kwargs: object.__new__(S3FileInput)
26 changes: 11 additions & 15 deletions s3file/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,8 @@ class S3FileInput(ClearableFileInput):
"""FileInput that uses JavaScript to directly upload to Amazon S3."""

needs_multipart_form = False
mime_type = None

def __init__(self, attrs=None):
self.expires = settings.SESSION_COOKIE_AGE
self.upload_path = getattr(settings, 'S3FILE_UPLOAD_PATH', os.path.join('tmp', 's3file'))
super(S3FileInput, self).__init__(attrs=attrs)
try:
self.mime_type = self.attrs['accept']
except KeyError:
pass
upload_path = getattr(settings, 'S3FILE_UPLOAD_PATH', os.path.join('tmp', 's3file'))
expires = settings.SESSION_COOKIE_AGE

@property
def bucket_name(self):
Expand All @@ -35,35 +27,39 @@ def client(self):

def build_attrs(self, *args, **kwargs):
attrs = super(S3FileInput, self).build_attrs(*args, **kwargs)

mime_type = attrs.get('accept', None)
response = self.client.generate_presigned_post(
self.bucket_name, os.path.join(self.upload_folder, '${filename}'),
Conditions=self.get_conditions(),
Conditions=self.get_conditions(mime_type),
ExpiresIn=self.expires,
)

defaults = {
'data-fields-%s' % key: value
for key, value in response['fields'].items()
}
defaults['data-url'] = response['url']
defaults.update(attrs)

try:
defaults['class'] += ' s3file'
except KeyError:
defaults['class'] = 's3file'
return defaults

def get_conditions(self):
def get_conditions(self, mime_type):
conditions = [
{"bucket": self.bucket_name},
["starts-with", "$key", self.upload_folder],
{"success_action_status": "201"},
]
if self.mime_type:
top_type, sub_type = self.mime_type.split('/', 1)
if mime_type:
top_type, sub_type = mime_type.split('/', 1)
if sub_type == '*':
conditions.append(["starts-with", "$Content-Type", "%s/" % top_type])
else:
conditions.append({"Content-Type": self.mime_type})
conditions.append({"Content-Type": mime_type})
else:
conditions.append(["starts-with", "$Content-Type", ""])

Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

setup(
name='django-s3file',
version='2.0.0',
version='3.0.0',
description='A lightweight file uploader input for Django and Amazon S3',
author='codingjoe',
url='https://github.com/codingjoe/django-s3file',
Expand All @@ -18,12 +18,12 @@
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Software Development',
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
],
packages=['s3file'],
include_package_data=True,
install_requires=[
'django-storages',
'boto3',
],
)
9 changes: 4 additions & 5 deletions tests/test_apps.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import importlib

from django.forms import ClearableFileInput
from django import forms

from s3file.apps import S3FileConfig
from s3file.forms import S3FileInput


class TestS3FileConfig:
def test_ready(self, settings):
app = S3FileConfig('s3file', __import__('tests.testapp'))
app = S3FileConfig('s3file', importlib.import_module('tests.testapp'))
app.ready()
forms = importlib.import_module('django.forms')
assert forms.FileField.widget == ClearableFileInput
assert not isinstance(forms.ClearableFileInput(), S3FileInput)
settings.DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
app.ready()
assert forms.FileField.widget == S3FileInput
assert isinstance(forms.ClearableFileInput(), S3FileInput)
22 changes: 14 additions & 8 deletions tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest
from django.core.files.storage import default_storage
from django.forms import ClearableFileInput
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.support.expected_conditions import staleness_of
from selenium.webdriver.support.wait import WebDriverWait
Expand All @@ -25,11 +26,16 @@ def wait_for_page_load(driver, timeout=30):
)


class TestS3FileInput(object):
class TestS3FileInput:
@property
def url(self):
return reverse('upload')

@pytest.fixture(autouse=True)
def patch(self):
ClearableFileInput.__new__ = \
lambda cls, *args, **kwargs: object.__new__(S3FileInput)

@pytest.fixture
def freeze(self, monkeypatch):
"""Freeze datetime and UUID."""
Expand Down Expand Up @@ -86,7 +92,7 @@ def test_build_attr(self):
assert S3FileInput().build_attrs({'class': 'my-class'})['class'] == 'my-class s3file'

def test_get_conditions(self, freeze):
conditions = S3FileInput().get_conditions()
conditions = S3FileInput().get_conditions(None)
assert all(condition in conditions for condition in [
{"bucket": 'test-bucket'},
{"success_action_status": "201"},
Expand All @@ -96,19 +102,16 @@ def test_get_conditions(self, freeze):

def test_accept(self):
widget = S3FileInput()
assert widget.mime_type is None
assert 'accept' not in widget.render(name='file', value='test.jpg')
assert ["starts-with", "$Content-Type", ""] in widget.get_conditions()
assert ["starts-with", "$Content-Type", ""] in widget.get_conditions(None)

widget = S3FileInput(attrs={'accept': 'image/*'})
assert widget.mime_type == 'image/*'
assert 'accept="image/*"' in widget.render(name='file', value='test.jpg')
assert ["starts-with", "$Content-Type", "image/"] in widget.get_conditions()
assert ["starts-with", "$Content-Type", "image/"] in widget.get_conditions('image/*')

widget = S3FileInput(attrs={'accept': 'image/jpeg'})
assert widget.mime_type == 'image/jpeg'
assert 'accept="image/jpeg"' in widget.render(name='file', value='test.jpg')
assert {"Content-Type": 'image/jpeg'} in widget.get_conditions()
assert {"Content-Type": 'image/jpeg'} in widget.get_conditions('image/jpeg')

def test_no_js_error(self, driver, live_server):
driver.get(live_server + self.url)
Expand All @@ -129,3 +132,6 @@ def test_file_insert(self, request, driver, live_server, upload_file, freeze):
with pytest.raises(NoSuchElementException):
error = driver.find_element_by_xpath('//body[@JSError]')
pytest.fail(error.get_attribute('JSError'))

def test_media(self):
assert ClearableFileInput().media._js == ['s3file/js/s3file.js']
2 changes: 1 addition & 1 deletion tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from s3file.middleware import S3FileMiddleware


class TestS3FileMiddleware(object):
class TestS3FileMiddleware:

def test_get_files_from_storage(self):
content = b'test_get_files_from_storage'
Expand Down
File renamed without changes.
5 changes: 0 additions & 5 deletions tests/testapp/forms.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
from django import forms

from s3file.forms import S3FileInput

from .models import FileModel


class UploadForm(forms.ModelForm):
class Meta:
model = FileModel
fields = ('file',)
widgets = {
'file': S3FileInput
}
2 changes: 1 addition & 1 deletion tests/testapp/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
'tests.testapp',
)

DEFAULT_FILE_STORAGE = 'tests.testapp.storages.DummyS3Boto3Storage'
DEFAULT_FILE_STORAGE = 'tests.testapp.dummy_storage.DummyS3Boto3Storage'

MIDDLEWARE = MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py{27,35,36}-dj{18,110,111,master},qa
envlist = py{35,36}-dj{18,110,111,master},qa
[testenv]
setenv=
DISPLAY=:99.0
Expand Down