diff --git a/pybossa/api/__init__.py b/pybossa/api/__init__.py index b8b1ffe928..7de87a5fa4 100644 --- a/pybossa/api/__init__.py +++ b/pybossa/api/__init__.py @@ -94,14 +94,17 @@ def register_api(view, endpoint, url, pk='id', pk_type='int'): view_func=view_func, methods=['GET', 'PUT', 'DELETE', 'OPTIONS']) + register_api(ProjectAPI, 'api_project', '/project', pk='oid', pk_type='int') -register_api(ProjectStatsAPI, 'api_projectstats', '/projectstats', pk='oid', pk_type='int') +register_api(ProjectStatsAPI, 'api_projectstats', + '/projectstats', pk='oid', pk_type='int') register_api(CategoryAPI, 'api_category', '/category', pk='oid', pk_type='int') register_api(TaskAPI, 'api_task', '/task', pk='oid', pk_type='int') register_api(TaskRunAPI, 'api_taskrun', '/taskrun', pk='oid', pk_type='int') register_api(ResultAPI, 'api_result', '/result', pk='oid', pk_type='int') register_api(UserAPI, 'api_user', '/user', pk='oid', pk_type='int') -register_api(AnnouncementAPI, 'api_announcement', '/announcement', pk='oid', pk_type='int') +register_api(AnnouncementAPI, 'api_announcement', + '/announcement', pk='oid', pk_type='int') register_api(BlogpostAPI, 'api_blogpost', '/blogpost', pk='oid', pk_type='int') register_api(HelpingMaterialAPI, 'api_helpingmaterial', '/helpingmaterial', pk='oid', pk_type='int') @@ -226,12 +229,16 @@ def user_progress(project_id=None, short_name=None): project = project_repo.get(project_id) if project: - # For now, keep this version, but wait until redis cache is + # For now, keep this version, but wait until redis cache is # used here for task_runs too + external_uid = request.args.get('external_uid') query_attrs = dict(project_id=project.id) if current_user.is_anonymous: - query_attrs['user_ip'] = anonymizer.ip(request.remote_addr or - '127.0.0.1') + if external_uid is None: + anon_ip = request.remote_addr or '127.0.0.1' + query_attrs['user_ip'] = anonymizer.ip(anon_ip) + else: + query_attrs['external_uid'] = external_uid else: query_attrs['user_id'] = current_user.id taskrun_count = task_repo.count_task_runs_with(**query_attrs) @@ -271,7 +278,8 @@ def get_disqus_sso_api(): """Return remote_auth_s3 and api_key for disqus SSO.""" try: if current_user.is_authenticated: - message, timestamp, sig, pub_key = get_disqus_sso_payload(current_user) + message, timestamp, sig, pub_key = get_disqus_sso_payload( + current_user) else: message, timestamp, sig, pub_key = get_disqus_sso_payload(None) diff --git a/test/test_api/test_project_api.py b/test/test_api/test_project_api.py index b925a3ce21..e10f0aa182 100644 --- a/test/test_api/test_project_api.py +++ b/test/test_api/test_project_api.py @@ -22,7 +22,7 @@ from test_api import TestAPI from factories import (ProjectFactory, TaskFactory, TaskRunFactory, AnonymousTaskRunFactory, UserFactory, - CategoryFactory) + CategoryFactory, ExternalUidTaskRunFactory) from pybossa.repositories import ProjectRepository from pybossa.repositories import TaskRepository @@ -32,6 +32,7 @@ task_repo = TaskRepository(db) result_repo = ResultRepository(db) + class TestProjectAPI(TestAPI): def setUp(self): @@ -57,14 +58,16 @@ def create_result(self, n_results=1, n_answers=1, owner=None, else: return result_repo.get_by(project_id=1) - @with_context def test_project_query(self): """ Test API project query""" - project1 = ProjectFactory.create(updated='2015-01-01T14:37:30.642119', info={'total': 150, 'task_presenter': 'foo'}) - projects = ProjectFactory.create_batch(8, info={'total': 150, 'task_presenter': 'foo'}) + project1 = ProjectFactory.create( + updated='2015-01-01T14:37:30.642119', info={'total': 150, 'task_presenter': 'foo'}) + projects = ProjectFactory.create_batch( + 8, info={'total': 150, 'task_presenter': 'foo'}) - project2 = ProjectFactory.create(updated='3019-01-01T14:37:30.642119', info={'total': 150, 'task_presenter': 'foo'}) + project2 = ProjectFactory.create( + updated='3019-01-01T14:37:30.642119', info={'total': 150, 'task_presenter': 'foo'}) projects.insert(0, project1) projects.append(project2) res = self.app.get('/api/project') @@ -119,7 +122,8 @@ def test_project_query(self): res = self.app.get(url) data = json.loads(res.data) err_msg = "It should get the last item first." - assert data[0]['updated'] == projects[len(projects)-1].updated, (err_msg, data) + assert data[0]['updated'] == projects[len( + projects)-1].updated, (err_msg, data) # Orderby filter url = "/api/project?orderby=id&desc=true" @@ -144,7 +148,8 @@ def test_project_query(self): err_msg = "It should get the last item first." projects_by_id = sorted(projects, key=lambda x: x.id, reverse=False) for i in range(len(projects_by_id)): - assert projects_by_id[i].id == data[i]['id'], (projects_by_id[i].id, data[i]['id']) + assert projects_by_id[i].id == data[i]['id'], ( + projects_by_id[i].id, data[i]['id']) url = "/api/project?orderby=id&desc=true" res = self.app.get(url) @@ -152,7 +157,8 @@ def test_project_query(self): err_msg = "It should get the last item first." projects_by_id = sorted(projects, key=lambda x: x.id, reverse=True) for i in range(len(projects_by_id)): - assert projects_by_id[i].id == data[i]['id'], (projects_by_id[i].id, data[i]['id']) + assert projects_by_id[i].id == data[i]['id'], ( + projects_by_id[i].id, data[i]['id']) @with_context def test_project_query_with_context(self): @@ -168,7 +174,8 @@ def test_project_query_with_context(self): assert project_oc.id == project['id'], project assert project['owner_id'] == user.id, project - res = self.app.get('/api/project?api_key=' + user.api_key + '&offset=1') + res = self.app.get('/api/project?api_key=' + + user.api_key + '&offset=1') data = json.loads(res.data) assert len(data) == 0, data @@ -197,13 +204,13 @@ def test_project_query_with_context(self): data = json.loads(res.data) assert len(data) == 0, data - res = self.app.get("/api/project?all=1&limit=5&api_key=" + user_two.api_key) + res = self.app.get( + "/api/project?all=1&limit=5&api_key=" + user_two.api_key) data = json.loads(res.data) assert len(data) == 5, data for d in data: d['owner_id'] == user.id, d - # Keyset pagination url = "/api/project?limit=5&last_id=%s&api_key=%s" % (projects[3].id, user.api_key) @@ -231,13 +238,13 @@ def test_project_query_with_context(self): for d in data: d['owner_id'] == user.id, d - @with_context def test_query_project(self): """Test API query for project endpoint works""" ProjectFactory.create(short_name='test-app', name='My New Project') # Test for real field - res = self.app.get("/api/project?short_name=test-app", follow_redirects=True) + res = self.app.get("/api/project?short_name=test-app", + follow_redirects=True) data = json.loads(res.data) # Should return one result assert len(data) == 1, data @@ -250,7 +257,8 @@ def test_query_project(self): assert len(data) == 0, data # Multiple fields - res = self.app.get('/api/project?short_name=test-app&name=My New Project') + res = self.app.get( + '/api/project?short_name=test-app&name=My New Project') data = json.loads(res.data) # One result assert len(data) == 1, data @@ -258,7 +266,6 @@ def test_query_project(self): assert data[0]['short_name'] == 'test-app', data assert data[0]['name'] == 'My New Project', data - @with_context def test_query_project_with_context(self): """Test API query for project endpoint with context works""" @@ -295,7 +302,6 @@ def test_query_project_with_context(self): assert data[0]['short_name'] == 'test-app', data assert data[0]['owner_id'] == user.id, data[0] - # Valid field but wrong value url = "/api/project?short_name=wrongvalue&api_key=" + user.api_key res = self.app.get(url) @@ -337,7 +343,6 @@ def test_query_project_with_context(self): data = json.loads(res.data) assert len(data) == 1, len(data) - @with_context def test_project_post(self): """Test API project creation and auth""" @@ -406,7 +411,7 @@ def test_project_post(self): # Test that a default category is assigned to the project assert cat2.id == out.category_id, "No category assigned to project" - # now a real user with headers auth and non-existing category_id + # now a real user with headers auth and non-existing category_id headers = [('Authorization', users[1].api_key)] new_project3 = dict( name=name + '4', @@ -457,7 +462,7 @@ def test_project_post(self): data = {'name': 'My New Title', 'links': 'hateoas'} data = dict(name='My New Title', links='hateoas', info={}) datajson = json.dumps(data) - ## anonymous + # anonymous res = self.app.put('/api/project/%s' % id_, data=datajson) error_msg = 'Anonymous should not be allowed to update' assert_equal(res.status, '401 UNAUTHORIZED', error_msg) @@ -466,7 +471,7 @@ def test_project_post(self): assert error['action'] == 'PUT', error assert error['exception_cls'] == 'Unauthorized', error - ### real user but not allowed as not owner! + # real user but not allowed as not owner! non_owner = UserFactory.create() url = '/api/project/%s?api_key=%s' % (id_, non_owner.api_key) res = self.app.put(url, data=datajson) @@ -593,7 +598,7 @@ def test_project_post(self): assert err['exception_cls'] == 'AttributeError', err # test delete - ## anonymous + # anonymous res = self.app.delete('/api/project/%s' % id_, data=data) error_msg = 'Anonymous should not be allowed to delete' assert_equal(res.status, '401 UNAUTHORIZED', error_msg) @@ -601,7 +606,7 @@ def test_project_post(self): assert error['status'] == 'failed', error assert error['action'] == 'DELETE', error assert error['target'] == 'project', error - ### real user but not allowed as not owner! + # real user but not allowed as not owner! url = '/api/project/%s?api_key=%s' % (id_, non_owner.api_key) res = self.app.delete(url, data=datajson) error_msg = 'Should not be able to delete projects of others' @@ -632,7 +637,6 @@ def test_project_post(self): res = self.app.delete(url, data=datajson) assert res.status_code == 404, error - @with_context def test_project_post_invalid_short_name(self): """Test API project POST returns error if short_name is invalid (i.e. is @@ -658,7 +662,6 @@ def test_project_post_invalid_short_name(self): message = "Project short_name is not valid, as it's used by the system." assert error['exception_msg'] == message, error - @with_context def test_project_put_invalid_short_name(self): """Test API project PUT returns error if short_name is invalid (i.e. is @@ -670,7 +673,7 @@ def test_project_put_invalid_short_name(self): data = {'short_name': 'new'} datajson = json.dumps(data) res = self.app.put('/api/project/%s?api_key=%s' % (project.id, user.api_key), - data=datajson) + data=datajson) error = json.loads(res.data) assert res.status_code == 415, res.status_code assert error['status'] == 'failed', error @@ -680,7 +683,6 @@ def test_project_put_invalid_short_name(self): message = "Project short_name is not valid, as it's used by the system." assert error['exception_msg'] == message, error - @with_context def test_admin_project_post(self): """Test API project update/delete for ADMIN users""" @@ -692,7 +694,7 @@ def test_admin_project_post(self): # test update data = {'name': 'My New Title'} datajson = json.dumps(data) - ### admin user but not owner! + # admin user but not owner! url = '/api/project/%s?api_key=%s' % (project.id, admin.api_key) res = self.app.put(url, data=datajson, follow_redirects=True) @@ -741,7 +743,7 @@ def test_admin_project_post(self): assert err['action'] == 'DELETE', err assert err['exception_cls'] == 'AttributeError', err - ### DELETE success real user not owner! + # DELETE success real user not owner! res = self.app.delete(url, data=json.dumps(data)) assert_equal(res.status, '204 NO CONTENT', res.data) assert res.mimetype == 'application/json' @@ -756,7 +758,8 @@ def test_user_progress_anonymous(self): for task in tasks: taskruns.extend(AnonymousTaskRunFactory.create_batch(2, task=task)) - res = self.app.get('/api/project/1/userprogress', follow_redirects=True) + res = self.app.get('/api/project/1/userprogress', + follow_redirects=True) data = json.loads(res.data) print(data) @@ -767,9 +770,11 @@ def test_user_progress_anonymous(self): assert len(taskruns) == data['done'], data # Add a new TaskRun and check again - taskrun = AnonymousTaskRunFactory.create(task=tasks[0], info={'answer': 'hello'}) + taskrun = AnonymousTaskRunFactory.create( + task=tasks[0], info={'answer': 'hello'}) - res = self.app.get('/api/project/1/userprogress', follow_redirects=True) + res = self.app.get('/api/project/1/userprogress', + follow_redirects=True) data = json.loads(res.data) error_msg = "The reported total number of tasks is wrong" assert len(tasks) == data['total'], error_msg @@ -778,6 +783,54 @@ def test_user_progress_anonymous(self): assert len(taskruns) + 1 == data['done'], error_msg @with_context + def test_external_user_id_progress_anonymous(self): + """Test API userprogress as external_uid works""" + user = UserFactory.create() + project = ProjectFactory.create(owner=user) + tasks = TaskFactory.create_batch(5, project=project) + taskruns = [] + taskruns_external_2 = [] + for task in tasks: + taskruns.extend( + ExternalUidTaskRunFactory.create_batch(2, task=task)) + + for task in tasks: + taskruns_external_2.extend( + ExternalUidTaskRunFactory.create_batch(5, task=task, external_uid='test@token.com')) + + res = self.app.get('/api/project/1/userprogress?external_uid=1xa', + follow_redirects=True) + data = json.loads(res.data) + + error_msg = "The reported total number of tasks is wrong" + assert len(tasks) == data['total'], error_msg + + res = self.app.get('/api/project/1/userprogress?external_uid=test@token.com', + follow_redirects=True) + data_external_uid_2 = json.loads(res.data) + + error_msg = "The reported total number of tasks is wrong" + assert data['total'] == len(tasks), error_msg + + error_msg = "The reported number of task runs is wrong" + assert len(taskruns) == data['done'], data + + assert len(taskruns_external_2) == data_external_uid_2['done'], data + + # Add a new TaskRun and check again + taskrun = ExternalUidTaskRunFactory.create( + task=tasks[0], info={'answer': 'hello'}) + + res = self.app.get('/api/project/1/userprogress?external_uid=1xa', + follow_redirects=True) + data = json.loads(res.data) + error_msg = "The reported total number of tasks is wrong" + assert len(tasks) == data['total'], error_msg + + error_msg = "Number of done tasks is wrong: %s" % len(taskruns) + assert len(taskruns) + 1 == data['done'], error_msg + + @ with_context def test_user_progress_authenticated_user(self): """Test API userprogress as an authenticated user works""" user = UserFactory.create() @@ -785,7 +838,8 @@ def test_user_progress_authenticated_user(self): tasks = TaskFactory.create_batch(2, project=project) taskruns = [] for task in tasks: - taskruns.extend(TaskRunFactory.create_batch(2, task=task, user=user)) + taskruns.extend(TaskRunFactory.create_batch( + 2, task=task, user=user)) url = '/api/project/1/userprogress?api_key=%s' % user.api_key res = self.app.get(url, follow_redirects=True) @@ -793,7 +847,8 @@ def test_user_progress_authenticated_user(self): error_msg = "The reported total number of tasks is wrong" assert len(tasks) == data['total'], error_msg - url = '/api/project/%s/userprogress?api_key=%s' % (project.short_name, user.api_key) + url = '/api/project/%s/userprogress?api_key=%s' % ( + project.short_name, user.api_key) res = self.app.get(url, follow_redirects=True) data = json.loads(res.data) error_msg = "The reported total number of tasks is wrong" @@ -811,7 +866,8 @@ def test_user_progress_authenticated_user(self): assert len(taskruns) == data['done'], error_msg # Add a new TaskRun and check again - taskrun = TaskRunFactory.create(task=tasks[0], info={'answer': 'hello'}, user=user) + taskrun = TaskRunFactory.create( + task=tasks[0], info={'answer': 'hello'}, user=user) url = '/api/project/1/userprogress?api_key=%s' % user.api_key res = self.app.get(url, follow_redirects=True) @@ -822,8 +878,7 @@ def test_user_progress_authenticated_user(self): error_msg = "Number of done tasks is wrong: %s" % len(taskruns) assert len(taskruns) + 1 == data['done'], error_msg - - @with_context + @ with_context def test_delete_project_cascade(self): """Test API delete project deletes associated tasks and taskruns""" project = ProjectFactory.create() @@ -838,13 +893,13 @@ def test_delete_project_cascade(self): task_runs = task_repo.filter_task_runs_by(project_id=project.id) assert len(task_runs) == 0, "There should not be any task run" - - @with_context + @ with_context def test_newtask_allow_anonymous_contributors(self): """Test API get a newtask - allow anonymous contributors""" project = ProjectFactory.create() user = UserFactory.create() - tasks = TaskFactory.create_batch(2, project=project, info={'question': 'answer'}) + tasks = TaskFactory.create_batch( + 2, project=project, info={'question': 'answer'}) # All users are allowed to participate by default # As Anonymous user @@ -896,8 +951,7 @@ def test_newtask_allow_anonymous_contributors(self): err_msg = "There should be a question" assert task['info'].get('question') == 'answer', err_msg - - @with_context + @ with_context def test_newtask(self): """Test API project new_task method and authentication""" project = ProjectFactory.create() @@ -936,8 +990,8 @@ def test_newtask(self): res = self.app.get(url) assert res.data == b'{}', res.data - @with_context - @patch('pybossa.repositories.project_repository.uploader') + @ with_context + @ patch('pybossa.repositories.project_repository.uploader') def test_project_delete_deletes_zip_files(self, uploader): """Test API project delete deletes also zip files of tasks and taskruns""" admin = UserFactory.create() @@ -951,7 +1005,7 @@ def test_project_delete_deletes_zip_files(self, uploader): call('1_project1_task_run_csv.zip', 'user_1')] assert uploader.delete_file.call_args_list == expected - @with_context + @ with_context def test_project_post_with_reserved_fields_returns_error(self): user = UserFactory.create() CategoryFactory.create() @@ -974,13 +1028,13 @@ def test_project_post_with_reserved_fields_returns_error(self): error = json.loads(res.data) assert error['exception_msg'] == "Reserved keys in payload", error - @with_context + @ with_context def test_project_put_with_reserved_returns_error(self): user = UserFactory.create() project = ProjectFactory.create(owner=user) url = '/api/project/%s?api_key=%s' % (project.id, user.api_key) data = {'created': 'today', 'updated': 'now', - 'contacted': False, 'completed': False,'id': 222} + 'contacted': False, 'completed': False, 'id': 222} res = self.app.put(url, data=json.dumps(data)) @@ -988,7 +1042,7 @@ def test_project_put_with_reserved_returns_error(self): error = json.loads(res.data) assert error['exception_msg'] == "Reserved keys in payload", error - @with_context + @ with_context def test_project_post_with_published_attribute_is_forbidden(self): user = UserFactory.create() data = dict( @@ -1007,7 +1061,7 @@ def test_project_post_with_published_attribute_is_forbidden(self): assert res.status_code == 403, res.status_code assert error_msg == 'You cannot publish a project via the API', res.data - @with_context + @ with_context def test_project_update_with_published_attribute_is_forbidden(self): user = UserFactory.create() project = ProjectFactory.create(owner=user) @@ -1021,7 +1075,7 @@ def test_project_update_with_published_attribute_is_forbidden(self): assert res.status_code == 403, res.status_code assert error_msg == 'You cannot publish a project via the API', res.data - @with_context + @ with_context def test_project_delete_with_results(self): """Test API delete project with results cannot be deleted.""" result = self.create_result() @@ -1032,7 +1086,7 @@ def test_project_delete_with_results(self): res = self.app.delete(url) assert_equal(res.status, '403 FORBIDDEN', res.status) - @with_context + @ with_context def test_project_delete_with_results_var(self): """Test API delete project with results cannot be deleted by admin.""" root = UserFactory.create(admin=True) @@ -1044,8 +1098,8 @@ def test_project_delete_with_results_var(self): res = self.app.delete(url) assert_equal(res.status, '403 FORBIDDEN', res.status) - @with_context - @patch('pybossa.api.api_base.caching') + @ with_context + @ patch('pybossa.api.api_base.caching') def test_project_cache_post_is_refreshed(self, caching_mock): """Test API project cache is updated after POST.""" clean_project_mock = MagicMock() @@ -1059,9 +1113,8 @@ def test_project_cache_post_is_refreshed(self, caching_mock): project_id = json.loads(res.data)['id'] clean_project_mock.assert_called_with(project_id), res.data - - @with_context - @patch('pybossa.api.api_base.caching') + @ with_context + @ patch('pybossa.api.api_base.caching') def test_project_cache_put_is_refreshed(self, caching_mock): """Test API project cache is updated after PUT.""" clean_project_mock = MagicMock() @@ -1081,8 +1134,8 @@ def test_project_cache_put_is_refreshed(self, caching_mock): res = self.app.put(url, data=json.dumps(payload)) clean_project_mock.assert_called_with(project.id) - @with_context - @patch('pybossa.api.api_base.caching') + @ with_context + @ patch('pybossa.api.api_base.caching') def test_project_cache_delete_is_refreshed(self, caching_mock): """Test API project cache is updated after DEL.""" clean_project_mock = MagicMock() @@ -1093,7 +1146,7 @@ def test_project_cache_delete_is_refreshed(self, caching_mock): res = self.app.delete(url) clean_project_mock.assert_called_with(project.id) - @with_context + @ with_context def test_project_filter_by_category_works(self): """Test API project filter by category works.""" category = CategoryFactory.create()