Skip to content

Commit 4df350a

Browse files
authored
Monkey patch ClearableFileInput (#62)
* Monkey patch ClearableFileInput * Drop Python 2 support
1 parent 02ccea9 commit 4df350a

File tree

12 files changed

+47
-80
lines changed

12 files changed

+47
-80
lines changed

.travis.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ dist: trusty
44
cache:
55
- apt
66
- pip
7-
services:
8-
- redis
97
addons:
108
firefox: latest
119
apt:
@@ -14,13 +12,11 @@ addons:
1412
packages:
1513
- google-chrome-stable
1614
python:
17-
- '2.7'
1815
- '3.5'
1916
- '3.6'
2017
env:
2118
global:
22-
- GECKO_DRIVER_VERSION=v0.16.1
23-
- CHROME_DRIVER_VERSION=2.29
19+
- CHROME_DRIVER_VERSION=2.32
2420
matrix:
2521
- DJANGO=18
2622
- DJANGO=110

README.md

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
A lightweight file upload input for Django and Amazon S3.
44

5+
Django-S3File allows you to upload files directly AWS S3 effectively
6+
bypassing your application server. This allows you to avoid long running
7+
requests from large file uploads.
8+
59
[![PyPi Version](https://img.shields.io/pypi/v/django-s3file.svg)](https://pypi.python.org/pypi/django-s3file/)
610
[![Build Status](https://travis-ci.org/codingjoe/django-s3file.svg?branch=master)](https://travis-ci.org/codingjoe/django-s3file)
711
[![Test Coverage](https://coveralls.io/repos/codingjoe/django-s3file/badge.svg?branch=master)](https://coveralls.io/r/codingjoe/django-s3file)
@@ -11,8 +15,7 @@ A lightweight file upload input for Django and Amazon S3.
1115

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

1821
## Installation
@@ -46,37 +49,12 @@ MIDDLEWARE = (
4649

4750
## Usage
4851

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

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

55-
### Simple integrations
56-
57-
**forms.py**
58-
59-
```python
60-
from django import forms
61-
from django.db import models
62-
from s3file.forms import S3FileInput
63-
64-
65-
class ImageModel(models.Model):
66-
file = models.FileField(upload_to='path/to/files')
67-
68-
69-
class MyModelForm(forms.ModelForm):
70-
class Meta:
71-
model = ImageModel
72-
fields = ('file',)
73-
widgets = {
74-
'file': S3FileInput(attrs={'accept': 'image/*'})
75-
}
76-
```
77-
**Done!** No really, that's all that needs to be done.
78-
79-
8058
### Setting up the AWS S3 bucket
8159

8260
### Upload folder

s3file/apps.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
from django.apps import AppConfig
22

3-
try:
4-
from storages.backends.s3boto3 import S3Boto3Storage
5-
except ImportError:
6-
from storages.backends.s3boto import S3BotoStorage as S3BotoStorage
7-
83

94
class S3FileConfig(AppConfig):
105
name = 's3file'
116
verbose_name = 'S3File'
127

138
def ready(self):
14-
from django.forms import FileField
159
from django.core.files.storage import default_storage
10+
from storages.backends.s3boto3 import S3Boto3Storage
1611

1712
if isinstance(default_storage, S3Boto3Storage):
13+
from django import forms
1814
from .forms import S3FileInput
1915

20-
FileField.widget = S3FileInput
16+
forms.ClearableFileInput.__new__ = \
17+
lambda cls, *args, **kwargs: object.__new__(S3FileInput)

s3file/forms.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,8 @@ class S3FileInput(ClearableFileInput):
1414
"""FileInput that uses JavaScript to directly upload to Amazon S3."""
1515

1616
needs_multipart_form = False
17-
mime_type = None
18-
19-
def __init__(self, attrs=None):
20-
self.expires = settings.SESSION_COOKIE_AGE
21-
self.upload_path = getattr(settings, 'S3FILE_UPLOAD_PATH', os.path.join('tmp', 's3file'))
22-
super(S3FileInput, self).__init__(attrs=attrs)
23-
try:
24-
self.mime_type = self.attrs['accept']
25-
except KeyError:
26-
pass
17+
upload_path = getattr(settings, 'S3FILE_UPLOAD_PATH', os.path.join('tmp', 's3file'))
18+
expires = settings.SESSION_COOKIE_AGE
2719

2820
@property
2921
def bucket_name(self):
@@ -35,35 +27,39 @@ def client(self):
3527

3628
def build_attrs(self, *args, **kwargs):
3729
attrs = super(S3FileInput, self).build_attrs(*args, **kwargs)
30+
31+
mime_type = attrs.get('accept', None)
3832
response = self.client.generate_presigned_post(
3933
self.bucket_name, os.path.join(self.upload_folder, '${filename}'),
40-
Conditions=self.get_conditions(),
34+
Conditions=self.get_conditions(mime_type),
4135
ExpiresIn=self.expires,
4236
)
37+
4338
defaults = {
4439
'data-fields-%s' % key: value
4540
for key, value in response['fields'].items()
4641
}
4742
defaults['data-url'] = response['url']
4843
defaults.update(attrs)
44+
4945
try:
5046
defaults['class'] += ' s3file'
5147
except KeyError:
5248
defaults['class'] = 's3file'
5349
return defaults
5450

55-
def get_conditions(self):
51+
def get_conditions(self, mime_type):
5652
conditions = [
5753
{"bucket": self.bucket_name},
5854
["starts-with", "$key", self.upload_folder],
5955
{"success_action_status": "201"},
6056
]
61-
if self.mime_type:
62-
top_type, sub_type = self.mime_type.split('/', 1)
57+
if mime_type:
58+
top_type, sub_type = mime_type.split('/', 1)
6359
if sub_type == '*':
6460
conditions.append(["starts-with", "$Content-Type", "%s/" % top_type])
6561
else:
66-
conditions.append({"Content-Type": self.mime_type})
62+
conditions.append({"Content-Type": mime_type})
6763
else:
6864
conditions.append(["starts-with", "$Content-Type", ""])
6965

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
setup(
55
name='django-s3file',
6-
version='2.0.0',
6+
version='3.0.0',
77
description='A lightweight file uploader input for Django and Amazon S3',
88
author='codingjoe',
99
url='https://github.com/codingjoe/django-s3file',
@@ -18,12 +18,12 @@
1818
'Operating System :: OS Independent',
1919
'Programming Language :: Python',
2020
'Topic :: Software Development',
21-
"Programming Language :: Python :: 2",
2221
"Programming Language :: Python :: 3",
2322
],
2423
packages=['s3file'],
2524
include_package_data=True,
2625
install_requires=[
2726
'django-storages',
27+
'boto3',
2828
],
2929
)

tests/test_apps.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import importlib
22

3-
from django.forms import ClearableFileInput
3+
from django import forms
44

55
from s3file.apps import S3FileConfig
66
from s3file.forms import S3FileInput
77

88

99
class TestS3FileConfig:
1010
def test_ready(self, settings):
11-
app = S3FileConfig('s3file', __import__('tests.testapp'))
11+
app = S3FileConfig('s3file', importlib.import_module('tests.testapp'))
1212
app.ready()
13-
forms = importlib.import_module('django.forms')
14-
assert forms.FileField.widget == ClearableFileInput
13+
assert not isinstance(forms.ClearableFileInput(), S3FileInput)
1514
settings.DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
1615
app.ready()
17-
assert forms.FileField.widget == S3FileInput
16+
assert isinstance(forms.ClearableFileInput(), S3FileInput)

tests/test_forms.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44
from django.core.files.storage import default_storage
5+
from django.forms import ClearableFileInput
56
from selenium.common.exceptions import NoSuchElementException
67
from selenium.webdriver.support.expected_conditions import staleness_of
78
from selenium.webdriver.support.wait import WebDriverWait
@@ -25,11 +26,16 @@ def wait_for_page_load(driver, timeout=30):
2526
)
2627

2728

28-
class TestS3FileInput(object):
29+
class TestS3FileInput:
2930
@property
3031
def url(self):
3132
return reverse('upload')
3233

34+
@pytest.fixture(autouse=True)
35+
def patch(self):
36+
ClearableFileInput.__new__ = \
37+
lambda cls, *args, **kwargs: object.__new__(S3FileInput)
38+
3339
@pytest.fixture
3440
def freeze(self, monkeypatch):
3541
"""Freeze datetime and UUID."""
@@ -86,7 +92,7 @@ def test_build_attr(self):
8692
assert S3FileInput().build_attrs({'class': 'my-class'})['class'] == 'my-class s3file'
8793

8894
def test_get_conditions(self, freeze):
89-
conditions = S3FileInput().get_conditions()
95+
conditions = S3FileInput().get_conditions(None)
9096
assert all(condition in conditions for condition in [
9197
{"bucket": 'test-bucket'},
9298
{"success_action_status": "201"},
@@ -96,19 +102,16 @@ def test_get_conditions(self, freeze):
96102

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

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

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

113116
def test_no_js_error(self, driver, live_server):
114117
driver.get(live_server + self.url)
@@ -129,3 +132,6 @@ def test_file_insert(self, request, driver, live_server, upload_file, freeze):
129132
with pytest.raises(NoSuchElementException):
130133
error = driver.find_element_by_xpath('//body[@JSError]')
131134
pytest.fail(error.get_attribute('JSError'))
135+
136+
def test_media(self):
137+
assert ClearableFileInput().media._js == ['s3file/js/s3file.js']

tests/test_middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from s3file.middleware import S3FileMiddleware
66

77

8-
class TestS3FileMiddleware(object):
8+
class TestS3FileMiddleware:
99

1010
def test_get_files_from_storage(self):
1111
content = b'test_get_files_from_storage'
File renamed without changes.

tests/testapp/forms.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
from django import forms
22

3-
from s3file.forms import S3FileInput
4-
53
from .models import FileModel
64

75

86
class UploadForm(forms.ModelForm):
97
class Meta:
108
model = FileModel
119
fields = ('file',)
12-
widgets = {
13-
'file': S3FileInput
14-
}

0 commit comments

Comments
 (0)