Skip to content

Commit 2936db3

Browse files
committed
Merge pull request #679 from /issues/24/1
Fixes #24: Allow images to be uploaded when creating a new bug report.
2 parents ce4d83e + 3790df1 commit 2936db3

21 files changed

+531
-215
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ fabfile.py
77
*.db
88
docs/build/
99
screenshots/
10+
uploads/
1011

1112
# these are our 'dist' files
1213
# checking them into the repo is silly

CONTRIBUTING.md

+13-5
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ cd webcompat.com
206206
### Detailed setup
207207
#### Installing pip
208208
209-
We use `pip` to install other Python packages. You may need to install `pip` if you haven't do so for another project or Python development.
209+
We use `pip` to install other Python packages. You may need to install `pip` if you haven't do so for another project or Python development.
210210
211211
To determine if you need to, type the following command into the terminal:
212212
@@ -279,6 +279,14 @@ You can now edit `config.py` and
279279
280280
![Auth 404](https://i.cloudup.com/8FDA5bVc7l.png)
281281
282+
**Note**: If you see the following error,
283+
284+
```
285+
RuntimeError: no destination for set uploads
286+
```
287+
288+
That means your `config.py` is out of sync with `config.py.example`. You can copy over the `Image uploading settings` from config.py.example, as well as the `LOCALHOST = not PRODUCTION and not DEVELOPMENT` line.
289+
282290
### Starting The Server
283291
284292
``` bash
@@ -374,19 +382,19 @@ Our Python unit tests are vanilla flavored [`unittest`](https://docs.python.org/
374382
375383
Unit tests are preferred for features or functionality that are independent of the browser front-end, i.e., API responses, application routes, etc.
376384
377-
Important documentation links:
385+
Important documentation links:
378386
* [Writing nose tests](https://nose.readthedocs.org/en/latest/writing_tests.html)
379387
* [`unittest`](https://docs.python.org/2/library/unittest.html)
380388
* [Testing Flask](http://flask.pocoo.org/docs/0.10/testing/)
381389
382390
### JS Functional Tests
383391
384-
Functional tests are written in JavaScript, using [Intern](http://theintern.io/). There's a nice [guide on the Intern wiki](https://github.com/theintern/intern/wiki/Writing-Tests-with-Intern#functional-testing) that should explain enough to get you started.
392+
Functional tests are written in JavaScript, using [Intern](http://theintern.io/). There's a nice [guide on the Intern wiki](https://github.com/theintern/intern/wiki/Writing-Tests-with-Intern#functional-testing) that should explain enough to get you started.
385393
386394
Important documentation links:
387395
* [Leadfoot](https://theintern.github.io/leadfoot/): the library that drives the browser (via Selenium).
388-
* [ChaiJS](http://chaijs.com/api/assert/): the library used for assertions.
389-
* [Intern wiki](https://github.com/theintern/intern/wiki): contains useful examples.
396+
* [ChaiJS](http://chaijs.com/api/assert/): the library used for assertions.
397+
* [Intern wiki](https://github.com/theintern/intern/wiki): contains useful examples.
390398
391399
It's also recommended to look at the other test files in the `tests/functional` directory to see how things are commonly done.
392400

config.py.example

+14
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ SECRET_KEY = "meow"
2222
# the PRODUCTION and DEVELOPMENT env var is set in uwsgi.conf
2323
PRODUCTION = os.environ.get('PRODUCTION')
2424
DEVELOPMENT = os.environ.get('DEVELOPMENT')
25+
# Are we serving the app from localhost?
26+
LOCALHOST = not PRODUCTION and not DEVELOPMENT
2527

2628
# Cache settings
2729
if PRODUCTION:
@@ -36,6 +38,18 @@ else:
3638
RATELIMIT_STORAGE_URL = 'memory://'
3739
RATELIMIT_STRATEGY = 'moving-window'
3840

41+
# Image uploading settings
42+
# See http://pythonhosted.org/Flask-Uploads/#configuration
43+
if PRODUCTION:
44+
UPLOADS_DEFAULT_DEST = ''
45+
UPLOADS_DEFAULT_URL = ''
46+
elif DEVELOPMENT:
47+
UPLOADS_DEFAULT_DEST = ''
48+
UPLOADS_DEFAULT_URL = ''
49+
elif LOCALHOST:
50+
UPLOADS_DEFAULT_DEST = BASE_DIR
51+
UPLOADS_DEFAULT_URL = 'http://localhost:5000/'
52+
3953
DEBUG = False
4054

4155
if not PRODUCTION:

requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ Flask==0.10.1
33
Flask-Cache==0.13.1
44
Flask-Limiter==0.7.4
55
Flask-SQLAlchemy==1.0
6+
Flask-Uploads==0.1.3
7+
Flask-WTF==0.12
68
GitHub-Flask==0.3.4
79
WTForms==2.0.2
810
ua-parser==0.3.5

run.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020

2121
try:
2222
from webcompat import app
23-
except ImportError:
24-
raise ImportError(IMPORT_ERROR)
23+
except ImportError, e:
24+
raise ImportError('{0}\n\n{1}'.format(e, IMPORT_ERROR))
2525

2626

2727
BOT_HELP = '''

tests/functional/reporting.js

+67-54
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ define([
6868
.sleep(2000);
6969
},
7070

71-
'validation works': function() {
71+
'URL validation': function() {
7272
return this.remote
7373
.setFindTimeout(intern.config.wc.pageLoadTimeout)
7474
.get(require.toUrl(url + '?open=1'))
@@ -78,8 +78,8 @@ define([
7878
.end()
7979
.findByCssSelector('#url').click()
8080
.end()
81-
// wait a second
82-
.sleep(1000)
81+
// wait a bit
82+
.sleep(100)
8383
.findByXpath('//*[@id="new-report"]/div/form/div[1]/div[2]/div[1]').getAttribute('class')
8484
.then(function (className) {
8585
assert.include(className, 'js-form-error');
@@ -88,20 +88,25 @@ define([
8888
.end()
8989
.findByCssSelector('#url').type('sup')
9090
.end()
91-
// wait a second
92-
.sleep(1000)
91+
// wait a bit
92+
.sleep(100)
9393
// xpath to the #url formGroup
9494
.findByXpath('//*[@id="new-report"]/div/form/div[1]/div[2]/div[1]').getAttribute('class')
9595
.then(function (className) {
9696
assert.include(className, 'js-no-error');
9797
assert.notInclude(className, 'js-form-error');
9898
})
99-
.end()
100-
// click in the textarea to trigger validation for radios
99+
.end();
100+
},
101+
102+
'Problem type validation': function() {
103+
return this.remote
104+
.setFindTimeout(intern.config.wc.pageLoadTimeout)
105+
.get(require.toUrl(url + '?open=1'))
101106
.findByCssSelector('#description').click()
102107
.end()
103-
// wait a second
104-
.sleep(1000)
108+
// wait a bit
109+
.sleep(100)
105110
.findByXpath('//*[@id="new-report"]/div/form/div[1]/div[1]/fieldset').getAttribute('class')
106111
.then(function (className) {
107112
assert.include(className, 'js-form-error');
@@ -111,65 +116,73 @@ define([
111116
// pick a problem type
112117
.findByCssSelector('#problem_category-0').click()
113118
.end()
114-
// wait a second
115-
.sleep(1000)
119+
// wait a bit
120+
.sleep(100)
116121
// validation message should be removed now
117122
.findByXpath('//*[@id="new-report"]/div/form/div[1]/div[1]/fieldset').getAttribute('class')
118123
.then(function (className) {
119124
assert.include(className, 'js-no-error');
120125
assert.notInclude(className, 'js-form-error');
121126
})
127+
.end();
128+
},
129+
130+
'Image extension validation': function() {
131+
return this.remote
132+
.setFindTimeout(intern.config.wc.pageLoadTimeout)
133+
.get(require.toUrl(url + '?open=1'))
134+
.findByCssSelector('#image').type('/path/to/foo.hacks')
135+
.end()
136+
// wait a bit
137+
.sleep(100)
138+
.findByXpath('//*[@id="new-report"]/div/form/div[2]/div[2]').getAttribute('class')
139+
.then(function (className) {
140+
assert.include(className, 'js-form-error');
141+
assert.notInclude(className, 'js-no-error');
142+
})
143+
.end()
144+
// pick a valid file type
145+
.findByCssSelector('#image').type('/path/to/foo.jpg')
146+
.end()
147+
// wait a bit
148+
.sleep(100)
149+
// validation message should be removed now
150+
.findByXpath('//*[@id="new-report"]/div/form/div[2]/div[2]').getAttribute('class')
151+
.then(function (className) {
152+
assert.include(className, 'js-no-error');
153+
assert.notInclude(className, 'js-form-error');
154+
})
155+
.end();
156+
},
157+
158+
'Submits are enabled': function() {
159+
return this.remote
160+
.setFindTimeout(intern.config.wc.pageLoadTimeout)
161+
.get(require.toUrl(url + '?open=1'))
162+
// pick a valid file type
163+
.findByCssSelector('#image').type('/path/to/foo.png')
122164
.end()
123-
// now make sure the buttons aren't disabled anymore
165+
.findByCssSelector('#url').type('http://coolguy.biz')
166+
.end()
167+
// pick a problem type
168+
.findByCssSelector('#problem_category-0').click()
169+
.end()
170+
.findByCssSelector('#description').click()
171+
.end()
172+
// wait a bit
173+
.sleep(100)
174+
// now make sure the buttons aren't disabled anymore
124175
.findByCssSelector('#submitanon').getAttribute('class')
125176
.then(function (className) {
126177
assert.notInclude(className, 'is-disabled');
127178
})
128179
.end()
180+
// now make sure the buttons aren't disabled anymore
129181
.findByCssSelector('#submitgithub').getAttribute('class')
130182
.then(function (className) {
131183
assert.notInclude(className, 'is-disabled');
132-
});
133-
},
134-
135-
// 'anonymous report': function () {
136-
// var issueNumber;
137-
138-
// return this.remote
139-
// .setFindTimeout(intern.config.wc.pageLoadTimeout)
140-
// .get(require.toUrl(url + "?open=1"))
141-
// .findByCssSelector('#url').type('some broken URL')
142-
// .end()
143-
// .findByCssSelector('#summary').type('this site doesnt work ' + Math.random())
144-
// .end()
145-
// .findByCssSelector('#submitanon').click()
146-
// .end()
147-
// .findByCssSelector('.wc-content--body h2').getVisibleText()
148-
// .then(function (text) {
149-
// // Make sure we got to the /thanks/<number> route
150-
// assert.equal(text, 'Thank you.');
151-
// })
152-
// .end()
153-
// .findByCssSelector('.wc-content--body a').getVisibleText()
154-
// .then(function (text) {
155-
// // Grab the issue number from the end of the URL link
156-
// issueNumber = text.split('/').pop();
157-
// })
158-
// .end()
159-
// .findByCssSelector('.wc-content--body a').getVisibleText().click()
160-
// .end()
161-
// .findByCssSelector('.js-issue-title').getVisibleText()
162-
// .then(function (text) {
163-
// // Make sure GitHub has the correct title
164-
// assert.include(text, 'some broken URL - this site doesnt work');
165-
// })
166-
// .end()
167-
// .findByCssSelector('.gh-header-number').getVisibleText()
168-
// .then(function (text) {
169-
// // Make sure GitHub has the correct issue number
170-
// assert.equal(text, '#' + issueNumber);
171-
// })
172-
// }
173-
184+
})
185+
.end();
186+
}
174187
});
175188
});

tests/test_uploads.py

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# This Source Code Form is subject to the terms of the Mozilla Public
4+
# License, v. 2.0. If a copy of the MPL was not distributed with this
5+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
7+
'''Tests for image upload API endpoint.'''
8+
9+
import os.path
10+
import sys
11+
import unittest
12+
13+
from flask import Request
14+
from flaskext.uploads import TestingFileStorage
15+
from StringIO import StringIO
16+
from werkzeug import FileStorage
17+
from werkzeug.datastructures import MultiDict
18+
19+
# Add webcompat module to import path
20+
sys.path.append(os.path.realpath(os.pardir))
21+
22+
from webcompat import app
23+
24+
25+
class TestUploads(unittest.TestCase):
26+
# modified from http://prschmid.blogspot.com/2013/05/unit-testing-flask-file-uploads-without.html
27+
def setUp(self):
28+
app.config['TESTING'] = True
29+
self.app = app
30+
self.test_client = self.app.test_client()
31+
32+
def tearDown(self):
33+
pass
34+
35+
def testGet(self):
36+
'''Test that /upload/ doesn't let you GET.'''
37+
rv = self.test_client.get('/upload/')
38+
self.assertEqual(rv.status_code, 404)
39+
40+
def testBadUploads(self):
41+
# Loop over some files and the status codes that we are expecting
42+
for filename, status_code in (
43+
('foo.xxx', 415),
44+
('foo', 415),
45+
('foo.rb', 415)):
46+
47+
# The reason why we are defining it in here and not outside
48+
# this method is that we are setting the filename of the
49+
# TestingFileStorage to be the one in the for loop. This way
50+
# we can ensure that the filename that we are "uploading"
51+
# is the same as the one being used by the application
52+
class TestingRequest(Request):
53+
"""A testing request to use that will return a
54+
TestingFileStorage to test the uploading."""
55+
@property
56+
def files(self):
57+
d = MultiDict()
58+
d['image'] = TestingFileStorage(filename=filename)
59+
return d
60+
61+
self.app.request_class = TestingRequest
62+
test_client = self.app.test_client()
63+
64+
rv = test_client.post(
65+
'/upload/',
66+
data=dict(
67+
file=(StringIO('Fake image data'), filename),
68+
))
69+
self.assertEqual(rv.status_code, status_code)
70+
71+
def testGoodUploads(self):
72+
# Loop over some files and the URLs that we are expecting back
73+
for filename, status_code in (
74+
('foo.png', 201),
75+
('foo.jpg', 201),
76+
('foo.gif', 201),
77+
('foo.jpe', 201),
78+
('foo.jpeg', 201),
79+
('foo.bmp', 201)):
80+
81+
class TestingRequest(Request):
82+
"""A testing request to use that will return a
83+
TestingFileStorage to test the uploading."""
84+
@property
85+
def files(self):
86+
d = MultiDict()
87+
d['image'] = TestingFileStorage(filename=filename)
88+
return d
89+
90+
self.app.request_class = TestingRequest
91+
test_client = self.app.test_client()
92+
93+
rv = test_client.post(
94+
'/upload/',
95+
data=dict(
96+
file=(StringIO('Fake image data'), filename),
97+
))
98+
self.assertEqual(rv.status_code, status_code)
99+
100+
if __name__ == '__main__':
101+
unittest.main()

webcompat/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
# import views after we initialize our github object
2727
import webcompat.views
2828

29-
# register API blueprint
29+
# register blueprints
3030
from api.endpoints import api
31+
from api.uploads import uploads
32+
3133
app.register_blueprint(api)
34+
app.register_blueprint(uploads)

0 commit comments

Comments
 (0)