Skip to content

Commit

Permalink
RDISCROWD-5567 Enhance task guidelines images (#803)
Browse files Browse the repository at this point in the history
* added upload_task_guidelines_image route

* add security checks

* add errors to response

* implement uploading and file size check

* remove test flash

* add file size tests

* fix typo in test description

* added multiple file uploads test

* use send 413 response code on large file

* refactor errors to error

* switch to small images

* remove added files

* Delete setuplogins.py

* use magic number for max image upload size

* edit settings_local template

* edit settings_test template

* use default value for MAX_IMAGE_UPLOAD_SIZE_MB

* initial push for edge case tests

* refactor and fix edge cases test

* bump theme SHA

Co-authored-by: nsyed22 <nsyed22@bloomberg.net>
  • Loading branch information
n00rsy and nsyed22 committed Jan 19, 2023
1 parent c3f51db commit 6ec316e
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 2 deletions.
Binary file added dump.rdb
Binary file not shown.
2 changes: 2 additions & 0 deletions pybossa/settings_local.py.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -500,3 +500,5 @@ WIZARD_STEPS = OrderedDict([
'visible_checks': {'and': ['project_publish'], 'or': []},
})]
)

MAX_IMAGE_UPLOAD_SIZE_MB = 5
57 changes: 56 additions & 1 deletion pybossa/view/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@
import urllib.parse
from rq import Queue
from werkzeug.datastructures import MultiDict
from werkzeug.utils import secure_filename

import pybossa.sched as sched
from pybossa.core import (uploader, signer, sentinel, json_exporter,
csv_exporter, importer, db, task_json_exporter,
task_csv_exporter, anonymizer)
task_csv_exporter, anonymizer, csrf)
from pybossa.model import make_uuid
from pybossa.model.project import Project
from pybossa.model.category import Category
Expand Down Expand Up @@ -517,6 +518,60 @@ def clone(short_name):
project=project_sanitized
))

def is_editor_disabled():
return (not current_user.admin and
current_app.config.get(
'DISABLE_TASK_PRESENTER_EDITOR'))

def is_admin_or_owner(project):
return (current_user.admin or
(project.owner_id == current_user.id or
current_user.id in project.owners_ids))

@blueprint.route('/<short_name>/tasks/taskpresenterimageupload', methods=['GET', 'POST'])
@login_required
@admin_or_subadmin_required
@csrf.exempt
def upload_task_guidelines_image(short_name):
error = False
return_code = 200
project = project_by_shortname(short_name)

imgurls = []
if is_editor_disabled():
flash(gettext('Task presenter editor disabled!'), 'error')
error = True
return_code = 400
elif not is_admin_or_owner(project):
flash(gettext('Ooops! Only project owners can upload files.'), 'error')
error = True
return_code = 400
else:
for file in request.files.getlist("image"):
file_size_mb = file.seek(0, os.SEEK_END) / 1024 / 1024
file.seek(0, os.SEEK_SET)
file.filename = secure_filename(file.filename)
if file_size_mb < current_app.config.get('MAX_IMAGE_UPLOAD_SIZE_MB', 5):
container = "user_%s" % current_user.id
uploader.upload_file(file, container=container)
imgurls.append(get_avatar_url(
current_app.config.get('UPLOAD_METHOD'),
file.filename,
container,
current_app.config.get('AVATAR_ABSOLUTE')
))
else:
flash(gettext('File must be smaller than ' + str(current_app.config.get('MAX_IMAGE_UPLOAD_SIZE_MB', 5)) + ' MB.'))
error = True
return_code = 413

response = {
"imgurls" : imgurls,
"error": error
}

return jsonify(response), return_code

@blueprint.route('/<short_name>/tasks/taskpresentereditor', methods=['GET', 'POST'])
@login_required
@admin_or_subadmin_required
Expand Down
2 changes: 2 additions & 0 deletions settings_test.py.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,5 @@ COMPLETED_TASK_CLEANUP_DAYS = [
(90, "90 days"),
(180, "180 days")
]

MAX_IMAGE_UPLOAD_SIZE_MB = 5
Binary file added test/files/small-image1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/files/small-image2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
151 changes: 151 additions & 0 deletions test/test_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from test import db, Fixtures, with_context, with_context_settings, \
FakeResponse, mock_contributions_guard, with_request_context
from test.helper import web
from test.test_authorization import mock_current_user
from unittest.mock import patch, Mock, call, MagicMock
from flask import redirect
from itsdangerous import BadSignature
Expand Down Expand Up @@ -4693,6 +4694,156 @@ def test_48_task_presenter_editor_works_json(self, mock):
assert data['form']['guidelines'] == 'Some guidelines!', data


@with_context
@patch('pybossa.view.projects.uploader.upload_file', return_value=True)
def test_task_presenter_large_image_upload(self, mock):
"""Test API /tasks/taskpresenterimageupload should not upload images with size > 5 MB"""
print("running test_task_presenter_large_image_upload...")
user = UserFactory.create(id=500)
project = ProjectFactory.create(
short_name='test_project',
name='Test Project',
info={
'total': 150,
'task_presenter': 'foo',
'data_classification': dict(input_data="L4 - public", output_data="L4 - public"),
'kpi': 0.5,
'product': 'abc',
'subproduct': 'def',
},
owner=user)
headers = [('Authorization', user.api_key)]
with open('./test/files/small-image1.jpg', 'rb') as img:
imgStringIO = BytesIO(img.read())
with patch.dict(self.flask_app.config, {'MAX_IMAGE_UPLOAD_SIZE_MB': 0}):
# Call API method to upload image.
res = self.app.post('/project/{}/tasks/taskpresenterimageupload'.format(project.short_name), headers=headers, data={'image': (imgStringIO, 'large-image.jpg')})
res_data = json.loads(res.data)
assert res.status_code == 413, "POST image upload should yield 413"
assert len(res_data['imgurls']) == 0, "Successful count of uploaded images 0."
assert res_data['error'] == True, "There should be an error for a file larger than 5 MB."

@with_context
@patch('pybossa.view.projects.uploader.upload_file', return_value=True)
def test_task_presenter_image_upload(self, mock):
"""Test API /tasks/taskpresenterimageupload to upload a task presenter guidelines image"""
print("running test_task_presenter_image_upload...")
user = UserFactory.create(id=500)
project = ProjectFactory.create(
short_name='test_project',
name='Test Project',
info={
'total': 150,
'task_presenter': 'foo',
'data_classification': dict(input_data="L4 - public", output_data="L4 - public"),
'kpi': 0.5,
'product': 'abc',
'subproduct': 'def',
},
owner=user)
headers = [('Authorization', user.api_key)]
with open('./test/files/small-image1.jpg', 'rb') as img:
imgStringIO = BytesIO(img.read())
# Call API method to upload image.
res = self.app.post('/project/{}/tasks/taskpresenterimageupload'.format(project.short_name), headers=headers, data={'image': (imgStringIO, 'large-image.jpg')})
res_data = json.loads(res.data)
assert res.status_code == 200, "POST image upload should be successful"
assert len(res_data['imgurls']) == 1, "Successful count of uploaded images 1."
assert res_data['error'] == False, "There should be no errors for normal file upload"

@with_context
@patch('pybossa.view.projects.uploader.upload_file', return_value=True)
def test_task_presenter_multiple_image_upload(self, mock):
"""Test API /tasks/taskpresenterimageupload to upload multiple task presenter guidelines images"""
print("running test_task_presenter_multiple_image_upload...")
user = UserFactory.create(id=500)
project = ProjectFactory.create(
short_name='test_project',
name='Test Project',
info={
'total': 150,
'task_presenter': 'foo',
'data_classification': dict(input_data="L4 - public", output_data="L4 - public"),
'kpi': 0.5,
'product': 'abc',
'subproduct': 'def',
},
owner=user)
headers = [('Authorization', user.api_key)]
with open('./test/files/small-image1.jpg', 'rb') as img1:
imgStringIO1 = BytesIO(img1.read())
with open('./test/files/small-image2.jpg', 'rb') as img2:
imgStringIO2 = BytesIO(img2.read())
# Call API method to upload image.
res = self.app.post('/project/{}/tasks/taskpresenterimageupload'.format(project.short_name), headers=headers, data={'image':
[(imgStringIO1, 'img1.jpg'), (imgStringIO2, 'img2.jpg')]
})
res_data = json.loads(res.data)
assert res.status_code == 200, "POST image upload should be successful"
assert len(res_data['imgurls']) == 2, "Successful count of uploaded images 2."
assert res_data['error'] == False, "There should be no errors for normal file upload"

@with_context
@patch('pybossa.view.projects.is_admin_or_owner', return_value=False)
def test_task_presenter_image_upload_user_not_owner_or_admin(self, mock):
"""Test API /tasks/taskpresenterimageupload to upload a task presenter guidelines image"""
print("running test_task_presenter_image_upload_user_not_owner_or_admin...")
user = UserFactory.create(id=500)
project = ProjectFactory.create(
short_name='test_project',
name='Test Project',
info={
'total': 150,
'task_presenter': 'foo',
'data_classification': dict(input_data="L4 - public", output_data="L4 - public"),
'kpi': 0.5,
'product': 'abc',
'subproduct': 'def',
},
owner=user)
headers = [('Authorization', user.api_key)]
with open('./test/files/small-image1.jpg', 'rb') as img:
imgStringIO = BytesIO(img.read())
# Call API method to upload image.
res = self.app.post('/project/{}/tasks/taskpresenterimageupload'.format(project.short_name), headers=headers, data={'image': (imgStringIO, 'large-image.jpg')})
res_data = json.loads(res.data)
assert res.status_code == 400, "POST image upload should be successful"
assert len(res_data['imgurls']) == 0, "Image should not be uploaded."
assert res_data['error'] == True, "There should be an error since the user is not owner or admin"

mock_authenticated=mock_current_user(anonymous=False, admin=False, id=2)

@with_context
@patch('pybossa.view.projects.is_editor_disabled', return_value=True)
def test_task_presenter_image_upload_task_presenter_disabled(self, disable_editor):

"""Test API /tasks/taskpresenterimageupload to upload a task presenter guidelines image"""
print("running test_task_presenter_image_upload_task_presenter_disabled...")
user = UserFactory.create(id=2, admin=False)
project = ProjectFactory.create(
short_name='test_project',
name='Test Project',
info={
'total': 150,
'task_presenter': 'foo',
'data_classification': dict(input_data="L4 - public", output_data="L4 - public"),
'kpi': 0.5,
'product': 'abc',
'subproduct': 'def',
},
owner=user)

headers = [('Authorization', user.api_key)]
with open('./test/files/small-image1.jpg', 'rb') as img:
imgStringIO = BytesIO(img.read())
with patch.dict(self.flask_app.config, {'DISABLE_TASK_PRESENTER_EDITOR': True}):
# Call API method to upload image.
res = self.app.post('/project/{}/tasks/taskpresenterimageupload'.format(project.short_name), headers=headers, data={'image': (imgStringIO, 'large-image.jpg')})
res_data = json.loads(res.data)
assert res.status_code == 400, "POST image upload should be successful"
assert len(res_data['imgurls']) == 0, "Image should not be uploaded."
assert res_data['error'] == True, "There should be an error since the task presenter is disabled"

@with_context
@patch('pybossa.ckan.requests.get')
@patch('pybossa.view.projects.uploader.upload_file', return_value=True)
Expand Down

0 comments on commit 6ec316e

Please sign in to comment.