class TestResultRepository(Test): def setUp(self): super(TestResultRepository, self).setUp() self.result_repo = ResultRepository(db) def create_result(self, n_answers=1, filter_by=False): task = TaskFactory.create(n_answers=n_answers) TaskRunFactory.create(task=task) if filter_by: return self.result_repo.filter_by(project_id=1) else: return self.result_repo.get_by(project_id=1) def test_get_return_none_if_no_result(self): """Test get method returns None if there is no result with the specified id""" result = self.result_repo.get(2) assert result is None, result def test_get_returns_result(self): """Test get method returns a result if exists""" n_answers = 1 task = TaskFactory.create(n_answers=n_answers) task_run = TaskRunFactory.create(task=task) result = self.result_repo.filter_by(project_id=1) err_msg = "There should be a result" assert len(result) == 1, err_msg result = result[0] assert result.project_id == 1, err_msg assert result.task_id == task.id, err_msg assert len(result.task_run_ids) == n_answers, err_msg err_msg = "The task_run id is missing in the results array" for tr_id in result.task_run_ids: assert tr_id == task_run.id, err_msg def test_get_by_returns_result(self): """Test get_by method returns a result if exists""" n_answers = 1 task = TaskFactory.create(n_answers=n_answers) task_run = TaskRunFactory.create(task=task) result = self.result_repo.get_by(project_id=1) err_msg = "There should be a result" assert result.project_id == 1, err_msg assert result.task_id == task.id, err_msg assert len(result.task_run_ids) == n_answers, err_msg err_msg = "The task_run id is missing in the results array" for tr_id in result.task_run_ids: assert tr_id == task_run.id, err_msg def test_get_returns_result_after_increasig_redundancy(self): """Test get method returns a result if after increasing redundancy""" n_answers = 1 task = TaskFactory.create(n_answers=n_answers) task_run = TaskRunFactory.create(task=task) result = self.result_repo.filter_by(project_id=1) err_msg = "There should be a result" assert len(result) == 1, err_msg result = result[0] assert result.project_id == 1, err_msg assert result.task_id == task.id, err_msg assert len(result.task_run_ids) == n_answers, err_msg err_msg = "The task_run id is missing in the results array" for tr_id in result.task_run_ids: assert tr_id == task_run.id, err_msg # Increase redundancy tmp = task_repo.get_task(task.id) tmp.n_answers = 2 task_repo.update(task) err_msg = "There should be only one result" results = result_repo.filter_by(project_id=1) assert len(results) == 1, err_msg task_run_2 = TaskRunFactory.create(task=task) err_msg = "There should be 1 results" results = result_repo.filter_by(project_id=1) assert len(results) == 1, err_msg err_msg = "There should be 2 results" results = result_repo.filter_by(project_id=1, last_version=False) assert len(results) == 2, err_msg assert results[1].project_id == 1, err_msg assert results[1].task_id == task.id, err_msg err_msg = "First result should have only one task run ID" assert len(results[0].task_run_ids) == 1, err_msg err_msg = "Second result should have only two task run IDs" assert len(results[1].task_run_ids) == 2, err_msg err_msg = "The task_run id is missing in the results array" for tr_id in results[1].task_run_ids: assert tr_id in [task_run.id, task_run_2.id], err_msg def test_get_returns_no_result(self): """Test get method does not return a result if task not completed""" n_answers = 3 task = TaskFactory.create(n_answers=n_answers) TaskRunFactory.create(task=task) result = self.result_repo.filter_by(project_id=1) err_msg = "There should not be a result" assert len(result) == 0, err_msg def test_fulltext_search_result(self): """Test fulltext search in JSON info works.""" result = self.create_result() text = 'something word you me bar' data = {'foo': text} result.info = data self.result_repo.update(result) info = 'foo::word' res = self.result_repo.filter_by(info=info, fulltextsearch='1') assert len(res) == 1, len(res) assert res[0].info['foo'] == text, res[0] res = self.result_repo.filter_by(info=info) assert len(res) == 0, len(res) def test_fulltext_search_result_01(self): """Test fulltext search in JSON info works.""" result = self.create_result() text = 'something word you me bar' data = {'foo': text, 'bar': 'foo'} result.info = data self.result_repo.update(result) info = 'foo::word&bar|bar::foo' res = self.result_repo.filter_by(info=info, fulltextsearch='1') assert len(res) == 1, len(res) assert res[0].info['foo'] == text, res[0] def test_info_json_search_result(self): """Test search in JSON info works.""" result = self.create_result() text = 'bar' data = {'foo': text} result.info = data self.result_repo.update(result) info = 'foo::bar' res = self.result_repo.filter_by(info=info) assert len(res) == 1, len(res) assert res[0].info['foo'] == text, res[0] def test_update(self): """Test update persists the changes made to the result""" result = self.create_result() result.info = dict(new='value') self.result_repo.update(result) updated_result = self.result_repo.get(result.id) assert updated_result.info['new'] == 'value', updated_result def test_update_fails_if_integrity_error(self): """Test update raises a DBIntegrityError if the instance to be updated lacks a required value""" result = self.create_result() result.project_id = None assert_raises(DBIntegrityError, self.result_repo.update, result) def test_update_only_updates_results(self): """Test update raises a WrongObjectError when an object which is not a Result instance is updated""" bad_object = dict() assert_raises(WrongObjectError, self.result_repo.update, bad_object)
class TestResultAuthorization(Test): mock_anonymous = mock_current_user() mock_authenticated = mock_current_user(anonymous=False, admin=False, id=2) mock_pro = mock_current_user(anonymous=False, admin=False, id=2, pro=True) mock_admin = mock_current_user(anonymous=False, admin=True, id=1) mock_owner = mock_current_user(anonymous=False, admin=False, id=1) def setUp(self): super(TestResultAuthorization, self).setUp() self.result_repo = ResultRepository(db) def create_result(self, n_answers=1, filter_by=False): task = TaskFactory.create(n_answers=n_answers) TaskRunFactory.create(task=task) if filter_by: return self.result_repo.filter_by(project_id=1) else: return self.result_repo.get_by(project_id=1) @with_context @patch('pybossa.auth.current_user', new=mock_anonymous) def test_anonymous_user_can_read_result(self): """Test anonymous users can read results""" result = self.create_result() assert ensure_authorized_to('read', result) @with_context @patch('pybossa.auth.current_user', new=mock_authenticated) def test_auth_user_can_read_result(self): """Test auth users can read results""" result = self.create_result() assert ensure_authorized_to('read', result) @with_context @patch('pybossa.auth.current_user', new=mock_admin) def test_admin_user_can_read_result(self): """Test admin users can read results""" result = self.create_result() assert ensure_authorized_to('read', result) @with_context @patch('pybossa.auth.current_user', new=mock_anonymous) def test_anonymous_user_cannot_save_results(self): """Test anonymous users cannot save results of a specific project""" result = Result() assert_raises(Unauthorized, ensure_authorized_to, 'create', result) @with_context @patch('pybossa.auth.current_user', new=mock_authenticated) def test_authenticated_user_cannot_save_results(self): """Test authenticated users cannot save results of a specific project""" result = Result() assert_raises(Forbidden, ensure_authorized_to, 'create', result) @with_context @patch('pybossa.auth.current_user', new=mock_admin) def test_admin_user_cannot_save_results(self): """Test admin users cannot save results of a specific project""" result = Result() assert_raises(Forbidden, ensure_authorized_to, 'create', result) @with_context @patch('pybossa.auth.current_user', new=mock_anonymous) def test_anonymous_user_cannot_delete_results(self): """Test anonymous users cannot delete results of a specific project""" result = Result() assert_raises(Unauthorized, ensure_authorized_to, 'delete', result) @with_context @patch('pybossa.auth.current_user', new=mock_authenticated) def test_authenticated_user_cannot_delete_results(self): """Test authenticated users cannot delete results of a specific project""" result = Result() assert_raises(Forbidden, ensure_authorized_to, 'delete', result) @with_context @patch('pybossa.auth.current_user', new=mock_admin) def test_admin_user_cannot_delete_results(self): """Test admin users cannot delete results of a specific project""" result = Result() assert_raises(Forbidden, ensure_authorized_to, 'delete', result) @with_context @patch('pybossa.auth.current_user', new=mock_anonymous) def test_anonymous_user_cannot_update_results(self): """Test anonymous users cannot update results of a specific project""" result = self.create_result() assert_raises(Unauthorized, ensure_authorized_to, 'update', result) @with_context @patch('pybossa.auth.current_user', new=mock_authenticated) def test_auth_user_cannot_update_results(self): """Test auth users but not owner cannot update results of a specific project""" result = self.create_result() assert_raises(Forbidden, ensure_authorized_to, 'update', result) @with_context @patch('pybossa.auth.current_user', new=mock_owner) def test_auth_owner_can_update_results(self): """Test auth owner can update results of a specific project""" result = self.create_result() result.info = dict(new='value') assert ensure_authorized_to('update', result) updated_result = self.result_repo.get_by(id=result.id) err_msg = "The result has not been updated" assert updated_result.info['new'] == 'value', err_msg
class TestResultAPI(TestAPI): def setUp(self): super(TestResultAPI, self).setUp() self.result_repo = ResultRepository(db) def create_result(self, n_results=1, n_answers=1, owner=None, filter_by=False): if owner: owner = owner else: owner = UserFactory.create() project = ProjectFactory.create(owner=owner) tasks = [] for i in range(n_results): tasks.append(TaskFactory.create(n_answers=n_answers, project=project)) for i in range(n_answers): for task in tasks: TaskRunFactory.create(task=task, project=project) if filter_by: return self.result_repo.filter_by(project_id=1) else: return self.result_repo.get_by(project_id=1) @with_context def test_result_query_without_params(self): """ Test API Result query""" result = self.create_result(n_answers=10) res = self.app.get('/api/result') results = json.loads(res.data) assert len(results) == 1, results result = results[0] assert result['info'] is None, result assert len(result['task_run_ids']) == 10, result assert result['task_run_ids'] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], result assert result['project_id'] == 1, result assert result['task_id'] == 1, result assert result['created'] is not None, result # The output should have a mime-type: application/json assert res.mimetype == 'application/json', res @with_context def test_result_query_with_params(self): """Test API query for result with params works""" results = self.create_result(n_results=10, filter_by=True) # Test for real field res = self.app.get("/api/result?project_id=1") data = json.loads(res.data) # Should return one result assert len(data) == 10, data # Correct result assert data[0]['project_id'] == 1, data # Valid field but wrong value res = self.app.get("/api/result?project_id=99999999") data = json.loads(res.data) assert len(data) == 0, data # Multiple fields res = self.app.get('/api/result?project_id=1&task_id=1') data = json.loads(res.data) # One result assert len(data) == 1, data # Correct result assert data[0]['project_id'] == 1, data assert data[0]['task_id'] == 1, data # Limits res = self.app.get("/api/result?project_id=1&limit=5") data = json.loads(res.data) for item in data: assert item['project_id'] == 1, item assert len(data) == 5, len(data) # Keyset pagination url = "/api/result?project_id=1&limit=5&last_id=1" res = self.app.get(url) data = json.loads(res.data) for item in data: assert item['project_id'] == 1, item assert len(data) == 5, data assert data[0]['id'] == 2, data[0] @with_context def test_result_post(self): """Test API Result creation""" admin = UserFactory.create() user = UserFactory.create() non_owner = UserFactory.create() project = ProjectFactory.create(owner=user) data = dict(info='final result') # anonymous user # no api-key res = self.app.post('/api/result', data=json.dumps(data)) error_msg = 'Should not be allowed to create' assert_equal(res.status, '401 UNAUTHORIZED', error_msg) ### real user but not allowed as not owner! res = self.app.post('/api/result?api_key=' + non_owner.api_key, data=json.dumps(data)) error_msg = 'Should not be able to post tasks for projects of others' assert_equal(res.status, '403 FORBIDDEN', error_msg) # now a real user res = self.app.post('/api/result?api_key=' + user.api_key, data=json.dumps(data)) assert_equal(res.status, '403 FORBIDDEN', error_msg) # now the root user res = self.app.post('/api/result?api_key=' + admin.api_key, data=json.dumps(data)) assert_equal(res.status, '403 FORBIDDEN', error_msg) # POST with not JSON data url = '/api/result?api_key=%s' % user.api_key res = self.app.post(url, data=data) err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'POST', err assert err['exception_cls'] == 'ValueError', err # POST with not allowed args res = self.app.post(url + '&foo=bar', data=json.dumps(data)) err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'POST', err assert err['exception_cls'] == 'AttributeError', err # POST with fake data data['wrongfield'] = 13 res = self.app.post(url, data=json.dumps(data)) err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'POST', err assert err['exception_cls'] == 'TypeError', err def test_result_post_with_reserved_fields_returns_error(self): user = UserFactory.create() project = ProjectFactory.create(owner=user) data = {'created': 'today', 'id': 222, 'project_id': project.id} res = self.app.post('/api/result?api_key=' + user.api_key, data=json.dumps(data)) assert res.status_code == 400, res.status_code error = json.loads(res.data) assert error['exception_msg'] == "Reserved keys in payload", error def test_result_put_with_reserved_fields_returns_error(self): user = UserFactory.create() result = self.create_result(owner=user) print result url = '/api/result/%s?api_key=%s' % (result.id, user.api_key) data = {'created': 'today', 'project_id': 1, 'id': 222} res = self.app.put(url, data=json.dumps(data)) assert res.status_code == 400, res.status_code error = json.loads(res.data) assert error['exception_msg'] == "Reserved keys in payload", error @with_context def test_result_update(self): """Test API result update""" admin = UserFactory.create() user = UserFactory.create() non_owner = UserFactory.create() data = dict(info=dict(foo='bar')) datajson = json.dumps(data) result = self.create_result(owner=user) ## anonymous res = self.app.put('/api/result/%s' % result.id, data=datajson) assert_equal(res.status, '401 UNAUTHORIZED', res.status) ### real user but not allowed as not owner! url = '/api/result/%s?api_key=%s' % (result.id, non_owner.api_key) res = self.app.put(url, data=datajson) assert_equal(res.status, '403 FORBIDDEN', res.status) ### real user url = '/api/result/%s?api_key=%s' % (result.id, user.api_key) res = self.app.put(url, data=datajson) out = json.loads(res.data) assert_equal(res.status, '200 OK', res.data) assert_equal(result.info['foo'], data['info']['foo']) assert result.id == out['id'], out ### root res = self.app.put('/api/result/%s?api_key=%s' % (result.id, admin.api_key), data=datajson) assert_equal(res.status, '403 FORBIDDEN', res.status) # PUT with not JSON data res = self.app.put(url, data=None) err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'PUT', err assert err['exception_cls'] == 'ValueError', err # PUT with not allowed args res = self.app.put(url + "&foo=bar", data=json.dumps(data)) err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'PUT', err assert err['exception_cls'] == 'AttributeError', err # PUT with fake data data['wrongfield'] = 13 res = self.app.put(url, data=json.dumps(data)) err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'PUT', err assert err['exception_cls'] == 'TypeError', err @with_context def test_result_delete(self): """Test API result delete""" admin = UserFactory.create() user = UserFactory.create() non_owner = UserFactory.create() result = self.create_result(owner=user) ## anonymous res = self.app.delete('/api/result/%s' % result.id) error_msg = 'Anonymous should not be allowed to update' assert_equal(res.status, '401 UNAUTHORIZED', error_msg) ### real user but not allowed as not owner! url = '/api/result/%s?api_key=%s' % (result.id, non_owner.api_key) res = self.app.delete(url) error_msg = 'Should not be able to update tasks of others' assert_equal(res.status, '403 FORBIDDEN', error_msg) #### real user # DELETE with not allowed args res = self.app.delete(url + "&foo=bar") err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'DELETE', err assert err['exception_cls'] == 'AttributeError', err # DELETE returns 403 url = '/api/result/%s?api_key=%s' % (result.id, user.api_key) res = self.app.delete(url) assert_equal(res.status, '403 FORBIDDEN', res.data) #### root user url = '/api/result/%s?api_key=%s' % (result.id, admin.api_key) res = self.app.delete(url) assert_equal(res.status, '403 FORBIDDEN', res.data) @with_context def test_get_last_version(self): """Test API result returns always latest version.""" result = self.create_result() project = project_repo.get(result.project_id) task = task_repo.get_task(result.task_id) task.n_answers = 2 TaskRunFactory.create(task=task, project=project) result = result_repo.get_by(project_id=project.id) assert result.last_version is True, result.last_version result_id = result.id results = result_repo.filter_by(project_id=project.id, last_version=False) assert len(results) == 2, len(results) for r in results: if r.id == result_id: assert r.last_version is True, r.last_version else: assert r.last_version is False, r.last_version
class TestResultAuthorization(Test): mock_anonymous = mock_current_user() mock_authenticated = mock_current_user(anonymous=False, admin=False, id=2) mock_pro = mock_current_user(anonymous=False, admin=False, id=2, pro=True) mock_admin = mock_current_user(anonymous=False, admin=True, id=1) mock_owner = mock_current_user(anonymous=False, admin=False, id=1) def setUp(self): super(TestResultAuthorization, self).setUp() self.result_repo = ResultRepository(db) def create_result(self, n_answers=1, filter_by=False): task = TaskFactory.create(n_answers=n_answers) TaskRunFactory.create(task=task) if filter_by: return self.result_repo.filter_by(project_id=1) else: return self.result_repo.get_by(project_id=1) @patch('pybossa.auth.current_user', new=mock_anonymous) def test_anonymous_user_can_read_result(self): """Test anonymous users can read results""" result = self.create_result() assert ensure_authorized_to('read', result) @patch('pybossa.auth.current_user', new=mock_authenticated) def test_auth_user_can_read_result(self): """Test auth users can read results""" result = self.create_result() assert ensure_authorized_to('read', result) @patch('pybossa.auth.current_user', new=mock_admin) def test_admin_user_can_read_result(self): """Test admin users can read results""" result = self.create_result() assert ensure_authorized_to('read', result) @patch('pybossa.auth.current_user', new=mock_anonymous) def test_anonymous_user_cannot_save_results(self): """Test anonymous users cannot save results of a specific project""" result = Result() assert_raises(Unauthorized, ensure_authorized_to, 'create', result) @patch('pybossa.auth.current_user', new=mock_authenticated) def test_authenticated_user_cannot_save_results(self): """Test authenticated users cannot save results of a specific project""" result = Result() assert_raises(Forbidden, ensure_authorized_to, 'create', result) @patch('pybossa.auth.current_user', new=mock_admin) def test_admin_user_cannot_save_results(self): """Test admin users cannot save results of a specific project""" result = Result() assert_raises(Forbidden, ensure_authorized_to, 'create', result) @patch('pybossa.auth.current_user', new=mock_anonymous) def test_anonymous_user_cannot_delete_results(self): """Test anonymous users cannot delete results of a specific project""" result = Result() assert_raises(Unauthorized, ensure_authorized_to, 'delete', result) @patch('pybossa.auth.current_user', new=mock_authenticated) def test_authenticated_user_cannot_delete_results(self): """Test authenticated users cannot delete results of a specific project""" result = Result() assert_raises(Forbidden, ensure_authorized_to, 'delete', result) @patch('pybossa.auth.current_user', new=mock_admin) def test_admin_user_cannot_delete_results(self): """Test admin users cannot delete results of a specific project""" result = Result() assert_raises(Forbidden, ensure_authorized_to, 'delete', result) @patch('pybossa.auth.current_user', new=mock_anonymous) def test_anonymous_user_cannot_update_results(self): """Test anonymous users cannot update results of a specific project""" result = self.create_result() assert_raises(Unauthorized, ensure_authorized_to, 'update', result) @patch('pybossa.auth.current_user', new=mock_authenticated) def test_auth_user_cannot_update_results(self): """Test auth users but not owner cannot update results of a specific project""" result = self.create_result() assert_raises(Forbidden, ensure_authorized_to, 'update', result) @patch('pybossa.auth.current_user', new=mock_owner) def test_auth_owner_can_update_results(self): """Test auth owner can update results of a specific project""" result = self.create_result() result.info = dict(new='value') assert ensure_authorized_to('update', result) updated_result = self.result_repo.get_by(id=result.id) err_msg = "The result has not been updated" assert updated_result.info['new'] == 'value', err_msg
class TestResultAPI(TestAPI): def setUp(self): super(TestResultAPI, self).setUp() self.result_repo = ResultRepository(db) def create_result(self, n_results=1, n_answers=1, owner=None, filter_by=False): if owner: owner = owner else: owner = UserFactory.create() project = ProjectFactory.create(owner=owner) tasks = [] for i in range(n_results): tasks.append(TaskFactory.create(n_answers=n_answers, project=project)) for i in range(n_answers): for task in tasks: TaskRunFactory.create(task=task, project=project) if filter_by: return self.result_repo.filter_by(project_id=1) else: return self.result_repo.get_by(project_id=1) @with_context def test_result_query_without_params(self): """ Test API Result query""" result = self.create_result(n_answers=10) res = self.app.get("/api/result") results = json.loads(res.data) assert len(results) == 1, results result = results[0] assert result["info"] is None, result assert len(result["task_run_ids"]) == 10, result assert result["task_run_ids"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], result assert result["project_id"] == 1, result assert result["task_id"] == 1, result assert result["created"] is not None, result result = self.create_result(n_answers=10) result = result_repo.get(2) result.created = "2119-01-01T14:37:30.642119" result_repo.update(result) url = "/api/result?desc=true" res = self.app.get(url) data = json.loads(res.data) err_msg = "It should get the last item first." assert data[0]["created"] == "2119-01-01T14:37:30.642119", err_msg url = "/api/result" res = self.app.get(url) data = json.loads(res.data) err_msg = "It should get not the last item first." assert data[0]["created"] != "2119-01-01T14:37:30.642119", err_msg # The output should have a mime-type: application/json assert res.mimetype == "application/json", res @with_context def test_result_query_without_params_with_context(self): """ Test API Result query with context.""" result = self.create_result(n_answers=10) res = self.app.get("/api/result") results = json.loads(res.data) assert len(results) == 1, results result = results[0] assert result["info"] is None, result assert len(result["task_run_ids"]) == 10, result assert result["task_run_ids"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], result assert result["project_id"] == 1, result assert result["task_id"] == 1, result assert result["created"] is not None, result # The output should have a mime-type: application/json assert res.mimetype == "application/json", res @with_context def test_result_query_with_params(self): """Test API query for result with params works""" owner = UserFactory.create() results = self.create_result(n_results=10, filter_by=True, owner=owner) # Test for real field res = self.app.get("/api/result?api_key=" + owner.api_key) data = json.loads(res.data) # Should return one result assert len(data) == 10, data # Correct result assert data[0]["project_id"] == 1, data res = self.app.get("/api/project?api_key=" + owner.api_key) project = json.loads(res.data) assert len(project) == 1, project assert project[0]["owner_id"] == owner.id, project owner_two = UserFactory.create() res = self.app.get("/api/result?api_key=" + owner_two.api_key) data = json.loads(res.data) # Should return zero results assert len(data) == 0, data owner_two = UserFactory.create() res = self.app.get("/api/result?all=1&api_key=" + owner_two.api_key) data = json.loads(res.data) # Should return ten results assert len(data) == 10, data assert data[0]["project_id"] == 1, data # Valid field but wrong value url = "/api/result?project_id=99999999&api_key=" + owner.api_key res = self.app.get(url) data = json.loads(res.data) assert len(data) == 0, data # Multiple fields url = "/api/result?project_id=1&task_id=1&api_key=" + owner.api_key res = self.app.get(url) data = json.loads(res.data) # One result assert len(data) == 1, data # Correct result assert data[0]["project_id"] == 1, data assert data[0]["task_id"] == 1, data # Multiple fields url = "/api/result?project_id=1&task_id=1&api_key=" + owner_two.api_key res = self.app.get(url) data = json.loads(res.data) # Zero result assert len(data) == 0, data url = "/api/result?all=1&project_id=1&task_id=1&api_key=" + owner_two.api_key res = self.app.get(url) data = json.loads(res.data) # One result assert len(data) == 1, data # Correct result assert data[0]["project_id"] == 1, data assert data[0]["task_id"] == 1, data # Limits url = "/api/result?project_id=1&limit=5&api_key=" + owner.api_key res = self.app.get(url) data = json.loads(res.data) for item in data: assert item["project_id"] == 1, item assert len(data) == 5, len(data) # Limits url = "/api/result?project_id=1&limit=5&api_key=" + owner_two.api_key res = self.app.get(url) data = json.loads(res.data) assert len(data) == 0, data # Limits url = "/api/result?all=1&project_id=1&limit=5&api_key=" + owner_two.api_key res = self.app.get(url) data = json.loads(res.data) for item in data: assert item["project_id"] == 1, item assert len(data) == 5, len(data) # Keyset pagination url = "/api/result?project_id=1&limit=5&last_id=1&api_key=" + owner.api_key res = self.app.get(url) data = json.loads(res.data) for item in data: assert item["project_id"] == 1, item assert len(data) == 5, data assert data[0]["id"] == 2, data[0] # Keyset pagination url = "/api/result?project_id=1&limit=5&last_id=1&api_key=" + owner_two.api_key res = self.app.get(url) data = json.loads(res.data) assert len(data) == 0, data # Keyset pagination url = "/api/result?all=1&project_id=1&limit=5&last_id=1&api_key=" + owner.api_key res = self.app.get(url) data = json.loads(res.data) for item in data: assert item["project_id"] == 1, item assert len(data) == 5, data assert data[0]["id"] == 2, data[0] @with_context def test_result_post(self): """Test API Result creation""" admin = UserFactory.create() user = UserFactory.create() non_owner = UserFactory.create() project = ProjectFactory.create(owner=user) data = dict(info="final result") # anonymous user # no api-key res = self.app.post("/api/result", data=json.dumps(data)) error_msg = "Should not be allowed to create" assert_equal(res.status, "401 UNAUTHORIZED", error_msg) ### real user but not allowed as not owner! res = self.app.post("/api/result?api_key=" + non_owner.api_key, data=json.dumps(data)) error_msg = "Should not be able to post tasks for projects of others" assert_equal(res.status, "403 FORBIDDEN", error_msg) # now a real user res = self.app.post("/api/result?api_key=" + user.api_key, data=json.dumps(data)) assert_equal(res.status, "403 FORBIDDEN", error_msg) # now the root user res = self.app.post("/api/result?api_key=" + admin.api_key, data=json.dumps(data)) assert_equal(res.status, "403 FORBIDDEN", error_msg) # POST with not JSON data url = "/api/result?api_key=%s" % user.api_key res = self.app.post(url, data=data) err = json.loads(res.data) assert res.status_code == 415, err assert err["status"] == "failed", err assert err["target"] == "result", err assert err["action"] == "POST", err assert err["exception_cls"] == "ValueError", err # POST with not allowed args res = self.app.post(url + "&foo=bar", data=json.dumps(data)) err = json.loads(res.data) assert res.status_code == 415, err assert err["status"] == "failed", err assert err["target"] == "result", err assert err["action"] == "POST", err assert err["exception_cls"] == "AttributeError", err # POST with fake data data["wrongfield"] = 13 res = self.app.post(url, data=json.dumps(data)) err = json.loads(res.data) assert res.status_code == 415, err assert err["status"] == "failed", err assert err["target"] == "result", err assert err["action"] == "POST", err assert err["exception_cls"] == "TypeError", err def test_result_post_with_reserved_fields_returns_error(self): user = UserFactory.create() project = ProjectFactory.create(owner=user) data = {"created": "today", "id": 222, "project_id": project.id} res = self.app.post("/api/result?api_key=" + user.api_key, data=json.dumps(data)) assert res.status_code == 400, res.status_code error = json.loads(res.data) assert error["exception_msg"] == "Reserved keys in payload", error def test_result_put_with_reserved_fields_returns_error(self): user = UserFactory.create() result = self.create_result(owner=user) print result url = "/api/result/%s?api_key=%s" % (result.id, user.api_key) data = {"created": "today", "project_id": 1, "id": 222} res = self.app.put(url, data=json.dumps(data)) assert res.status_code == 400, res.status_code error = json.loads(res.data) assert error["exception_msg"] == "Reserved keys in payload", error @with_context def test_result_update(self): """Test API result update""" admin = UserFactory.create() user = UserFactory.create() non_owner = UserFactory.create() data = dict(info=dict(foo="bar")) datajson = json.dumps(data) result = self.create_result(owner=user) ## anonymous res = self.app.put("/api/result/%s" % result.id, data=datajson) assert_equal(res.status, "401 UNAUTHORIZED", res.status) ### real user but not allowed as not owner! url = "/api/result/%s?api_key=%s" % (result.id, non_owner.api_key) res = self.app.put(url, data=datajson) assert_equal(res.status, "403 FORBIDDEN", res.status) ### real user url = "/api/result/%s?api_key=%s" % (result.id, user.api_key) res = self.app.put(url, data=datajson) out = json.loads(res.data) assert_equal(res.status, "200 OK", res.data) assert_equal(result.info["foo"], data["info"]["foo"]) assert result.id == out["id"], out ### root res = self.app.put("/api/result/%s?api_key=%s" % (result.id, admin.api_key), data=datajson) assert_equal(res.status, "403 FORBIDDEN", res.status) # PUT with not JSON data res = self.app.put(url, data=None) err = json.loads(res.data) assert res.status_code == 415, err assert err["status"] == "failed", err assert err["target"] == "result", err assert err["action"] == "PUT", err assert err["exception_cls"] == "ValueError", err # PUT with not allowed args res = self.app.put(url + "&foo=bar", data=json.dumps(data)) err = json.loads(res.data) assert res.status_code == 415, err assert err["status"] == "failed", err assert err["target"] == "result", err assert err["action"] == "PUT", err assert err["exception_cls"] == "AttributeError", err # PUT with fake data data["wrongfield"] = 13 res = self.app.put(url, data=json.dumps(data)) err = json.loads(res.data) assert res.status_code == 415, err assert err["status"] == "failed", err assert err["target"] == "result", err assert err["action"] == "PUT", err assert err["exception_cls"] == "TypeError", err @with_context def test_result_delete(self): """Test API result delete""" admin = UserFactory.create() user = UserFactory.create() non_owner = UserFactory.create() result = self.create_result(owner=user) ## anonymous res = self.app.delete("/api/result/%s" % result.id) error_msg = "Anonymous should not be allowed to update" assert_equal(res.status, "401 UNAUTHORIZED", error_msg) ### real user but not allowed as not owner! url = "/api/result/%s?api_key=%s" % (result.id, non_owner.api_key) res = self.app.delete(url) error_msg = "Should not be able to update tasks of others" assert_equal(res.status, "403 FORBIDDEN", error_msg) #### real user # DELETE with not allowed args res = self.app.delete(url + "&foo=bar") err = json.loads(res.data) assert res.status_code == 415, err assert err["status"] == "failed", err assert err["target"] == "result", err assert err["action"] == "DELETE", err assert err["exception_cls"] == "AttributeError", err # DELETE returns 403 url = "/api/result/%s?api_key=%s" % (result.id, user.api_key) res = self.app.delete(url) assert_equal(res.status, "403 FORBIDDEN", res.data) #### root user url = "/api/result/%s?api_key=%s" % (result.id, admin.api_key) res = self.app.delete(url) assert_equal(res.status, "403 FORBIDDEN", res.data) @with_context def test_get_last_version(self): """Test API result returns always latest version.""" result = self.create_result() project = project_repo.get(result.project_id) task = task_repo.get_task(result.task_id) task.n_answers = 2 TaskRunFactory.create(task=task, project=project) result = result_repo.get_by(project_id=project.id) assert result.last_version is True, result.last_version result_id = result.id results = result_repo.filter_by(project_id=project.id, last_version=False) assert len(results) == 2, len(results) for r in results: if r.id == result_id: assert r.last_version is True, r.last_version else: assert r.last_version is False, r.last_version
class TestZ3950Analyst(Test): def setUp(self): super(TestZ3950Analyst, self).setUp() self.ctx = ContextFixtures() self.z3950_analyst = Z3950Analyst() self.result_repo = ResultRepository(db) self.task_repo = TaskRepository(db) self.data = { 'user_id': [1], 'control_number': ['123'], 'reference': ['abc'], 'foo': ['bar'], 'comments': ['Some comment'] } def test_get_comments(self): """Test Z3950 comments are returned.""" task_run_df = pandas.DataFrame(self.data) comments = self.z3950_analyst.get_comments(task_run_df) expected = [(self.data['user_id'][i], self.data['comments'][i]) for i in range(len(self.data['user_id']))] assert_equal(comments, expected) def test_get_tags(self): """Test Z3950 tags are returned.""" task_run_df = pandas.DataFrame(self.data) tags = self.z3950_analyst.get_tags(task_run_df) assert_dict_equal(tags, {}) def test_get_transcriptions_df(self): """Test Z3950 transcriptions are returned.""" task_run_df = pandas.DataFrame(self.data) df = self.z3950_analyst.get_transcriptions_df(task_run_df) assert_dict_equal( df.to_dict(), { 'control_number': dict(enumerate(self.data['control_number'])), 'reference': dict(enumerate(self.data['reference'])) }) @with_context @patch('pybossa_lc.model.base.wa_client') def test_analysis_with_no_transcriptions(self, mock_client): """Test Z3950 analysis with no transcriptions.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target) TaskRunFactory.create_batch(n_answers, task=task, info={ 'control_number': '', 'reference': '', 'comments': '' }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) assert_equal(mock_client.create_annotation.called, False) @with_context @patch('pybossa_lc.model.base.wa_client') def test_analysis_with_no_transcriptions_and_old_keys(self, mock_client): """Test Z3950 analysis with no transcriptions and old keys.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target) TaskRunFactory.create_batch(n_answers, task=task, info={ 'oclc': '', 'shelfmark': '', 'comments': '' }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) assert_equal(mock_client.create_annotation.called, False) @with_context @patch('pybossa_lc.model.base.wa_client') def test_analysis_with_no_reference(self, mock_client): """Test Z3950 analysis with no reference.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target) TaskRunFactory.create_batch(n_answers, task=task, info={ 'control_number': 'foo', 'reference': '', 'comments': '' }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) assert_equal(mock_client.create_annotation.called, False) @with_context @patch('pybossa_lc.model.base.wa_client') def test_analysis_with_no_control_number(self, mock_client): """Test Z3950 analysis with no control number.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target) TaskRunFactory.create_batch(n_answers, task=task, info={ 'control_number': '', 'reference': 'foo', 'comments': '' }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) assert_equal(mock_client.create_annotation.called, False) @with_context @patch('pybossa_lc.model.base.wa_client') def test_comment_annotation_created(self, mock_client): """Test Z3950 comment annotations are created.""" n_answers = 1 target = 'example.com' anno_collection = 'http://eg.com/collection' task = self.ctx.create_task(n_answers, target, anno_collection=anno_collection) user = UserFactory() value = 'foo' TaskRunFactory.create_batch(n_answers, user=user, task=task, info={ 'control_number': '', 'reference': '', 'comments': value }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) func = mock_client.create_annotation func.assert_called_once_with( anno_collection, { 'motivation': 'commenting', 'type': 'Annotation', 'creator': { 'id': url_for('api.api_user', oid=user.id), 'type': 'Person', 'name': user.fullname, 'nickname': user.name }, 'generator': [{ "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" }], 'body': { 'type': 'TextualBody', 'purpose': 'commenting', 'value': value, 'format': 'text/plain' }, 'target': target }) @with_context @patch('pybossa_lc.model.base.wa_client') def test_transcriptions_are_normalised(self, mock_client): """Test Z3950 transcriptions are normalised according to set rules.""" n_answers = 1 target = 'example.com' anno_collection = 'http://eg.com/collection' rules = dict(case='title', whitespace='full_stop', trim_punctuation=True) task = self.ctx.create_task(n_answers, target, rules=rules, anno_collection=anno_collection) control_number = 'foo' references = ['OR 123 456.', 'Or.123.456. ', 'or 123 456'] for value in references: TaskRunFactory.create(task=task, info={ 'reference': value, 'control_number': control_number, 'comments': '' }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) assert_equal( mock_client.create_annotation.call_args_list, [ call( anno_collection, { 'motivation': 'describing', 'type': 'Annotation', 'generator': [{ "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" }], 'body': [{ 'type': 'TextualBody', 'purpose': 'describing', 'value': control_number.capitalize(), 'format': 'text/plain' }, { 'type': 'TextualBody', 'purpose': 'tagging', 'value': 'control_number' }], 'target': target }), call( anno_collection, { 'motivation': 'describing', 'type': 'Annotation', 'generator': [ { "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" } ], 'body': [{ 'type': 'TextualBody', 'purpose': 'describing', 'value': 'Or.123.456', 'format': 'text/plain' }, { 'type': 'TextualBody', 'purpose': 'tagging', 'value': 'reference' }], 'target': target }) ]) @with_context @patch('pybossa_lc.model.base.wa_client') def test_with_matching_transcriptions(self, mock_client): """Test Z3950 results with matching transcriptions.""" n_answers = 3 target = 'example.com' anno_collection = 'http://eg.com/collection' task = self.ctx.create_task(n_answers, target, rules={}, anno_collection=anno_collection) reference = 'foo' control_number = 'bar' TaskRunFactory.create_batch(n_answers, task=task, info={ 'reference': reference, 'control_number': control_number, 'comments': '' }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) func = mock_client.create_annotation assert_equal( mock_client.create_annotation.call_args_list, [ call( anno_collection, { 'motivation': 'describing', 'type': 'Annotation', 'generator': [{ "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" }], 'body': [{ 'type': 'TextualBody', 'purpose': 'describing', 'value': control_number, 'format': 'text/plain' }, { 'type': 'TextualBody', 'purpose': 'tagging', 'value': 'control_number' }], 'target': target }), call( anno_collection, { 'motivation': 'describing', 'type': 'Annotation', 'generator': [ { "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" } ], 'body': [{ 'type': 'TextualBody', 'purpose': 'describing', 'value': reference, 'format': 'text/plain' }, { 'type': 'TextualBody', 'purpose': 'tagging', 'value': 'reference' }], 'target': target }) ]) @with_context @patch('pybossa_lc.model.base.wa_client') def test_redundancy_increased_when_not_max(self, mock_client): """Test Z3950 task redundancy is updated when max not reached.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target, max_answers=4) for i in range(n_answers): TaskRunFactory.create(task=task, info={ 'reference': i, 'control_number': i, 'comments': '' }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) assert_equal(mock_client.create_annotation.called, False) updated_task = self.task_repo.get_task(task.id) assert_equal(updated_task.n_answers, n_answers + 1) @with_context @patch('pybossa_lc.model.base.wa_client') def test_redundancy_not_increased_when_max(self, mock_client): """Test Z3950 task redundancy is not updated when max is reached.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target, max_answers=3) for i in range(n_answers): TaskRunFactory.create(task=task, info={ 'reference': i, 'control_number': i, 'comments': '' }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) assert_equal(mock_client.create_annotation.called, False) updated_task = self.task_repo.get_task(task.id) assert_equal(updated_task.n_answers, n_answers) @with_context @patch('pybossa_lc.model.base.wa_client') def test_redundancy_not_increased_for_comments(self, mock_client): """Test Z3950 task redundancy is not updated for comments.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target, max_answers=n_answers + 1) for i in range(n_answers): TaskRunFactory.create(task=task, info={ 'reference': 'foo', 'control_number': 'bar', 'comments': i }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) updated_task = self.task_repo.get_task(task.id) assert_equal(updated_task.n_answers, n_answers) @with_context @patch('pybossa_lc.model.base.wa_client') def test_redundancy_not_increased_when_no_values(self, mock_client): """Test Z3950 task redundancy is not updated when no values.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target, max_answers=n_answers + 1) for i in range(n_answers): TaskRunFactory.create(task=task, info={ 'reference': '', 'control_number': '', 'comments': '' }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) updated_task = self.task_repo.get_task(task.id) assert_equal(updated_task.n_answers, n_answers) assert_equal(mock_client.create_annotation.called, False) @with_context @patch('pybossa_lc.model.base.wa_client') def test_old_annotations_deleted(self, mock_client): """Test Z3950 old Annotations deleted.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target) TaskRunFactory.create_batch(n_answers, task=task, info={ 'reference': '', 'control_number': '', 'comments': '' }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_annos = [{'id': 'baz'}, {'id': 'qux'}] fake_search = MagicMock() fake_search.return_value = fake_annos mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id, analyse_full=True) mock_client.delete_batch.assert_called_once_with(fake_annos) @with_context @patch('pybossa_lc.model.base.wa_client') def test_results_with_annotations_not_analysed(self, mock_client): """Test results with Annotations already not analysed by default.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target) TaskRunFactory.create_batch(n_answers, task=task, info={ 'reference': 'foo', 'control_number': 'bar', 'comments': '' }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_annos = [{'id': 'baz'}, {'id': 'qux'}] fake_search = MagicMock() fake_search.return_value = fake_annos mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) assert_equal(mock_client.delete_batch.called, False) @with_context @patch('pybossa_lc.model.base.wa_client') def test_annotation_collection_iri_added_to_result_info(self, mock_client): """Test Z3950 result info updated with AnnotationCollection IRI.""" n_answers = 3 target = 'example.com' anno_collection = 'annotations.example.com' task = self.ctx.create_task(n_answers, target, anno_collection=anno_collection) TaskRunFactory.create_batch(n_answers, task=task, info={ 'reference': 'foo', 'control_number': 'bar', 'comments': '' }) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) assert_dict_equal(result.info, {'annotations': anno_collection}) @with_context @patch('pybossa_lc.model.base.wa_client') def test_task_rejected(self, mock_client): """Test Z3950 annotation not created when task rejected * n_answers.""" n_answers = 3 target = 'example.com' anno_collection = 'annotations.example.com' task = self.ctx.create_task(n_answers, target, anno_collection=anno_collection) user = UserFactory() reason = 'invalid-task' TaskRunFactory.create_batch(n_answers, user=user, task=task, info={'reject': reason}) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.z3950_analyst.analyse(result.id) assert not mock_client.create_annotation.called assert_dict_equal(result.info, { 'annotations': anno_collection, 'rejected': reason })
class TestBaseAnalyst(Test): def setUp(self): super(TestBaseAnalyst, self).setUp() BaseAnalyst.__abstractmethods__ = frozenset() self.ctx = ContextFixtures() self.base_analyst = BaseAnalyst() self.project_repo = ProjectRepository(db) self.result_repo = ResultRepository(db) self.task_repo = TaskRepository(db) assert_dict_equal.__self__.maxDiff = None assert_equal.__self__.maxDiff = None @with_context @patch("pybossa_lc.analysis.base.BaseAnalyst.analyse") def test_analyse_all(self, mock_analyse): """Test that all results are analysed.""" project = ProjectFactory() tasks = TaskFactory.create_batch(2, project=project, n_answers=1) for task in tasks: TaskRunFactory.create(task=task) result = self.result_repo.get_by(task_id=tasks[0].id) result.info = dict(annotations=[{}]) self.result_repo.update(result) self.base_analyst.analyse_all(project.id) expected = [call(t.id, analyse_full=True) for t in tasks] assert_equal(mock_analyse.call_args_list, expected) @with_context @patch("pybossa_lc.analysis.base.BaseAnalyst.analyse") def test_analyse_empty(self, mock_analyse): """Test that empty results are analysed.""" project = ProjectFactory() tasks = TaskFactory.create_batch(2, project=project, n_answers=1) for task in tasks: TaskRunFactory.create(task=task) result = self.result_repo.get_by(task_id=tasks[0].id) result.info = dict(annotations=[{}]) self.result_repo.update(result) all_results = self.result_repo.filter_by(project_id=project.id) self.base_analyst.analyse_empty(project.id) expected = [call(r.task_id) for r in all_results if not r.info] assert_equal(mock_analyse.call_args_list, expected) @with_context def test_key_dropped(self): """Test the correct keys are dropped.""" data = [{'foo': None, 'bar': None}] df = pandas.DataFrame(data, range(len(data))) excluded = ['foo'] df = self.base_analyst.drop_keys(df, excluded) assert_not_in('foo', df.keys()) assert_in('bar', df.keys()) @with_context def test_empty_rows_dropped(self): """Test empty rows are dropped.""" data = [{'foo': 'bar'}, {'foo': None}] df = pandas.DataFrame(data, range(len(data))) df = self.base_analyst.drop_empty_rows(df) assert_equals(df['foo'].tolist(), ['bar']) @with_context def test_partial_rows_not_dropped(self): """Test partial rows are not dropped.""" data = [{'foo': 'bar', 'baz': None}] df = pandas.DataFrame(data, range(len(data))) df = self.base_analyst.drop_empty_rows(df) expected = {'foo': {0: 'bar'}, 'baz': {0: None}} assert_dict_equal(df.to_dict(), expected) @with_context def test_match_fails_when_percentage_not_met(self): """Test False is returned when min answers not met.""" data = [{'foo': 'bar', 'baz': None}] df = pandas.DataFrame(data, range(len(data))) min_answers = 2 has_matches = self.base_analyst.has_n_matches(min_answers, df) assert_equal(has_matches, False) @with_context def test_match_fails_when_nan_cols(self): """Test False is returned when NaN columns only.""" data = [{'foo': None}] df = pandas.DataFrame(data, range(len(data))) df = df.replace('', numpy.nan) min_answers = 2 has_matches = self.base_analyst.has_n_matches(min_answers, df) assert_equal(has_matches, False) @with_context def test_match_succeeds_when_percentage_met(self): """Test True returned when match percentage met.""" data = [{'foo': 'bar'}, {'foo': 'bar'}] df = pandas.DataFrame(data, range(len(data))) min_answers = 2 has_matches = self.base_analyst.has_n_matches(min_answers, df) assert_equal(has_matches, True) @with_context def test_get_dataframe_with_dict(self): """Test the task run dataframe with a dict as the info.""" info = {'foo': 'bar'} n_task_runs = 2 task = TaskFactory() taskruns = TaskRunFactory.create_batch(n_task_runs, task=task, info=info) df = self.base_analyst.get_task_run_df(task, taskruns) assert_equal(df['foo'].tolist(), [info['foo']] * n_task_runs) assert_equal(df['info'].tolist(), [info] * n_task_runs) @with_context def test_get_dataframe_with_list(self): """Test the task run dataframe with a list as the info.""" info = [{'foo': 'bar'}, {'baz': 'qux'}] n_task_runs = 2 task = TaskFactory() taskruns = TaskRunFactory.create_batch(n_task_runs, task=task, info=info) df = self.base_analyst.get_task_run_df(task, taskruns) assert_equal(df['info'].tolist(), [info] * n_task_runs) @with_context def test_protected_keys_prefixed_when_exploded(self): """Test that protected info keys are prefixed.""" info = {'foo': 'bar', 'info': 'baz'} task = TaskFactory() taskrun = TaskRunFactory.create(task=task, info=info) df = self.base_analyst.get_task_run_df(task, [taskrun]) assert_equal(df['_info'].tolist(), [info['info']]) @with_context def test_user_ids_in_task_run_dataframe(self): """Test that user IDs are included in the task run dataframe.""" task = TaskFactory() taskruns = TaskRunFactory.create_batch(2, task=task) df = self.base_analyst.get_task_run_df(task, taskruns) assert_equal(df['user_id'].tolist(), [tr.user_id for tr in taskruns]) def test_titlecase_normalisation(self): """Test titlecase normalisation.""" rules = dict(case='title') norm = self.base_analyst.normalise_transcription('Some words', rules) assert_equal(norm, 'Some Words') def test_lowercase_normalisation(self): """Test lowercase normalisation.""" rules = dict(case='lower') norm = self.base_analyst.normalise_transcription('Some words', rules) assert_equal(norm, 'some words') def test_uppercase_normalisation(self): """Test uppercase normalisation.""" rules = dict(case='upper') norm = self.base_analyst.normalise_transcription('Some words', rules) assert_equal(norm, 'SOME WORDS') def test_whitespace_normalisation(self): """Test whitespace normalisation.""" rules = dict(whitespace='normalise') norm = self.base_analyst.normalise_transcription(' Two Words', rules) assert_equal(norm, 'Two Words') def test_whitespace_replace_underscore(self): """Test replacing whitespace with underscore normalisation.""" rules = dict(whitespace='underscore') norm = self.base_analyst.normalise_transcription(' Two Words', rules) assert_equal(norm, 'Two_Words') def test_whitespace_replace_full_stop(self): """Test replacing whitespace with full stop normalisation.""" rules = dict(whitespace='full_stop') norm = self.base_analyst.normalise_transcription(' Two Words', rules) assert_equal(norm, 'Two.Words') def test_trim_punctuation_normalisation(self): """Test trim punctuation normalisation.""" rules = dict(trim_punctuation=True) norm = self.base_analyst.normalise_transcription(':Oh, a word.', rules) assert_equal(norm, 'Oh, a word') def test_date_not_normalised_if_rule_inactive(self): """Test date conversion not applied of rule not activate.""" norm = self.base_analyst.normalise_transcription('foo', {}) assert_equal(norm, 'foo') def test_date_conversion_with_slash(self): """Test date conversion with slash seperators.""" rules = dict(date_format=True, dayfirst=True) norm = self.base_analyst.normalise_transcription('19/11/1984', rules) assert_equal(norm, '1984-11-19') def test_date_conversion_with_hyphen(self): """Test date conversion with hyphen seperator.""" rules = dict(date_format=True, dayfirst=True) norm = self.base_analyst.normalise_transcription('19-11-1984', rules) assert_equal(norm, '1984-11-19') def test_date_conversion_with_no_seperator(self): """Test date conversion with no seperator.""" rules = dict(date_format=True, dayfirst=True) norm = self.base_analyst.normalise_transcription('19111984', rules) assert_equal(norm, '') def test_date_conversion_with_no_year_and_year_last(self): """Test date conversion with no year and year last.""" rules = dict(date_format=True, dayfirst=True) norm = self.base_analyst.normalise_transcription('19/11', rules) assert_equal(norm, '-11-19') def test_date_conversion_with_no_year_and_year_first(self): """Test date conversion with no year and year first.""" rules = dict(date_format=True, yearfirst=True) norm = self.base_analyst.normalise_transcription('11/19', rules) assert_equal(norm, '-11-19') def test_date_conversion_with_invalid_string(self): """Test date conversion with invalid string.""" rules = dict(date_format=True, dayfirst=True) norm = self.base_analyst.normalise_transcription('No date', rules) assert_equal(norm, '') def test_date_conversion_with_zero(self): """Test date conversion with zero.""" rules = dict(date_format=True, dayfirst=True) norm = self.base_analyst.normalise_transcription('0', rules) assert_equal(norm, '') def test_date_conversion_with_non_zero_integer(self): """Test date conversion with non-zero integer.""" rules = dict(date_format=True, dayfirst=True) norm = self.base_analyst.normalise_transcription('1', rules) assert_equal(norm, '') def test_date_conversion_with_trailing_punctuation(self): """Test date conversion with trailing punctuation.""" rules = dict(date_format=True, dayfirst=True) norm = self.base_analyst.normalise_transcription('19/11/', rules) assert_equal(norm, '-11-19') def test_date_conversion_with_trailing_whitespace(self): """Test date conversion with trailing whitespace.""" rules = dict(date_format=True, dayfirst=True) norm = self.base_analyst.normalise_transcription('19/11/1984 ', rules) assert_equal(norm, '1984-11-19') @with_context def test_n_answers_increased_when_task_complete(self): """Test n answers required for a task is updated.""" n_original_answers = 1 task = TaskFactory.create(n_answers=n_original_answers) TaskRunFactory.create(task=task) self.base_analyst.update_n_answers_required(task, False) assert_equal(task.n_answers, n_original_answers + 1) assert_equal(task.state, 'ongoing') @with_context def test_n_answers_not_increased_when_still_task_runs(self): """Test n answers not updated when task runs still required.""" n_original_answers = 2 task = TaskFactory.create(n_answers=n_original_answers) TaskRunFactory.create(task=task) self.base_analyst.update_n_answers_required(task, False) assert_equal(task.n_answers, n_original_answers) assert_equal(task.state, 'ongoing') @with_context def test_n_answers_not_increased_when_max_answers_reached(self): """Test n answers not updated when max answers reached.""" n_answers = 3 task = TaskFactory.create(n_answers=n_answers) TaskRunFactory.create_batch(n_answers, task=task) self.base_analyst.update_n_answers_required(task, False, max_answers=n_answers) assert_equal(task.n_answers, n_answers) assert_equal(task.state, 'completed') @with_context def test_n_answers_reduced_when_task_complete(self): """Test n answers reduced to number of task runs when task complete.""" n_answers = 3 task = TaskFactory.create(n_answers=n_answers) TaskRunFactory.create_batch(n_answers - 1, task=task) self.base_analyst.update_n_answers_required(task, True, max_answers=n_answers) assert_equal(task.n_answers, n_answers - 1) assert_equal(task.state, 'completed') def test_overlap_ratio_is_1_with_equal_rects(self): """Test for an overlap ratio of 1.""" rect = {'x': 100, 'y': 100, 'w': 100, 'h': 100} overlap = self.base_analyst.get_overlap_ratio(rect, rect) assert_equal(overlap, 1) def test_overlap_ratio_is_0_with_adjacent_rects(self): """Test for an overlap ratio of 0.""" r1 = {'x': 100, 'y': 100, 'w': 100, 'h': 100} r2 = {'x': 100, 'y': 201, 'w': 100, 'h': 100} overlap = self.base_analyst.get_overlap_ratio(r1, r2) assert_equal(overlap, 0) def test_overlap_ratio_with_partially_overlapping_rects(self): """Test for an overlap ratio of 0.33.""" r1 = {'x': 100, 'y': 100, 'w': 100, 'h': 100} r2 = {'x': 150, 'y': 100, 'w': 100, 'h': 100} overlap = self.base_analyst.get_overlap_ratio(r1, r2) assert_equal('{:.2f}'.format(overlap), '0.33') def test_overlap_ratio_where_union_is_zero(self): """Test for an overlap ratio where the union is zero.""" r1 = {'x': 0, 'y': 0, 'w': 100, 'h': 100} r2 = {'x': 101, 'y': 0, 'w': 100, 'h': 100} overlap = self.base_analyst.get_overlap_ratio(r1, r2) assert_equal(overlap, 0) def test_rect_from_selection(self): """Test that we get the correct rect.""" coords = dict(x=400, y=200, w=100, h=150) coords_str = '{0},{1},{2},{3}'.format(coords['x'], coords['y'], coords['w'], coords['h']) fake_anno = { 'target': { 'selector': { 'value': '?xywh={}'.format(coords_str) } } } rect = self.base_analyst.get_rect_from_selection_anno(fake_anno) assert_dict_equal(rect, coords) def test_rect_from_selection_with_floats(self): """Test that we get the correct rect with rounded coordinates.""" coords = dict(x=400.001, y=200.499, w=100.501, h=150.999) coords_str = '{0},{1},{2},{3}'.format(coords['x'], coords['y'], coords['w'], coords['h']) fake_anno = { 'target': { 'selector': { 'value': '?xywh={}'.format(coords_str) } } } rect = self.base_analyst.get_rect_from_selection_anno(fake_anno) assert_dict_equal(rect, {'x': 400, 'y': 200, 'w': 101, 'h': 151}) @with_context def test_get_project_template(self): """Test that the correct template is returned.""" category = CategoryFactory() tmpl_fixtures = TemplateFixtures(category) tmpl1 = tmpl_fixtures.create() tmpl2 = tmpl_fixtures.create() fake_templates = [tmpl1, tmpl2] category.info = dict(templates=fake_templates) self.project_repo.update_category(category) project_info = dict(template_id=tmpl1['id']) project = ProjectFactory(category=category, info=project_info) ret_tmpl = self.base_analyst.get_project_template(project) assert_equal(ret_tmpl, tmpl1) @with_context @raises(ValueError) def test_get_invalid_project_template(self): """Test that getting an invalid template throws an error.""" fake_templates = [{'id': 'foo'}] user_info = dict(templates=fake_templates) project_info = dict(template_id='bar') UserFactory.create(info=user_info) project = ProjectFactory(info=project_info) self.base_analyst.get_project_template(project) @with_context @raises(ValueError) def test_get_non_existant_project_template(self): """Test that getting a non-existant template throws an error.""" project = ProjectFactory() self.base_analyst.get_project_template(project) def test_dataframe_keys_replaced(self): """Test that dataframe keys are replaced and columns merged.""" data = [{'foo': '你好', 'baz': 'qux'}, {'foo': 1, 'quux': 'qux'}] old_df = pandas.DataFrame(data, range(len(data))) new_df = self.base_analyst.replace_df_keys(old_df, quux='baz') assert_dict_equal(new_df.to_dict(), { 'foo': { 0: '你好', 1: 1 }, 'baz': { 0: 'qux', 1: 'qux' } }) @with_context @patch('pybossa_lc.analysis.base.send_mail') @patch('pybossa_lc.analysis.base.render_template') @patch('pybossa_lc.analysis.base.Queue.enqueue') def test_comment_annotations_emailed(self, mock_enqueue, mock_render, mock_send_mail): """Test that comment annotation emails are sent.""" mock_render.return_value = True comment = 'foo' creator = 'bar' target = 'example.com' fake_anno = { 'creator': { 'id': 'example.com/user1', 'type': 'Person', 'name': creator, 'nickname': 'nick' }, 'body': { 'type': 'TextualBody', 'purpose': 'commenting', 'value': comment, 'format': 'text/plain' } } task = self.ctx.create_task(1, target) json_anno = json.dumps(fake_anno, indent=2, sort_keys=True) self.base_analyst.email_comment_anno(task, fake_anno) expected_render_args = [ call('/account/email/new_comment_anno.md', annotation=json_anno, creator=creator, comment=comment, raw_image=None, link=None), call('/account/email/new_comment_anno.html', annotation=json_anno, creator=creator, comment=comment, raw_image=None, link=None) ] assert_equal(mock_render.call_args_list, expected_render_args) expected_msg = { 'body': True, 'html': True, 'subject': 'New Comment Annotation', 'recipients': flask_app.config.get('ADMINS') } mock_enqueue.assert_called_once_with(mock_send_mail, expected_msg) @with_context @patch('pybossa_lc.model.base.wa_client') def test_modified_results_not_updated(self, mock_client): """Test results are not updated if an Annotation has been modified.""" task = self.ctx.create_task(1) TaskRunFactory(task=task) result = self.result_repo.get_by(task_id=task.id) self.base_analyst.analyse(result.id) mock_client.search_annotations.return_value = [{ 'modified': 'fake-time' }] assert_equal(mock_client.create_annotation.called, False) @with_context @patch('pybossa_lc.model.base.wa_client') def test_parent_results_not_updated(self, mock_client): """Test results are not updated if an Annotation has children.""" task = self.ctx.create_task(1) TaskRunFactory(task=task) result = self.result_repo.get_by(task_id=task.id) result.info = dict(has_children=True) self.result_repo.update(result) self.base_analyst.analyse(result.id) assert_equal(mock_client.create_annotation.called, False) @with_context @patch('pybossa_lc.model.base.wa_client') def test_result_with_child_not_updated(self, mock_client): """Test that a result is not updated when it has a child.""" task = self.ctx.create_task(1) TaskRunFactory(task=task) result = self.result_repo.get_by(task_id=task.id) info = dict(annotations='foo', has_children=True) result.info = info self.result_repo.update(result) self.base_analyst.analyse(result.id) assert_equal(result.info, info) @with_context def test_analysis_exception_if_no_annotation_collection(self): """Test that AnnotationCollection must be setup.""" task = self.ctx.create_task(1, 'example.com', anno_collection=None) TaskRunFactory.create(task=task) result = self.result_repo.filter_by(project_id=task.project_id)[0] assert_raises(AnalysisException, self.base_analyst.analyse, result.id)
class TestResultRepository(Test): def setUp(self): super(TestResultRepository, self).setUp() self.result_repo = ResultRepository(db) @with_context def create_result(self, n_answers=1, filter_by=False): task = TaskFactory.create(n_answers=n_answers) TaskRunFactory.create(task=task) if filter_by: return self.result_repo.filter_by(project_id=1) else: return self.result_repo.get_by(project_id=1) @with_context def test_get_return_none_if_no_result(self): """Test get method returns None if there is no result with the specified id""" result = self.result_repo.get(2) assert result is None, result @with_context def test_get_returns_result(self): """Test get method returns a result if exists""" n_answers = 1 task = TaskFactory.create(n_answers=n_answers) task_run = TaskRunFactory.create(task=task) result = self.result_repo.filter_by(project_id=1) err_msg = "There should be a result" assert len(result) == 1, err_msg result = result[0] assert result.project_id == 1, err_msg assert result.task_id == task.id, err_msg assert len(result.task_run_ids) == n_answers, err_msg err_msg = "The task_run id is missing in the results array" for tr_id in result.task_run_ids: assert tr_id == task_run.id, err_msg @with_context def test_get_by_returns_result(self): """Test get_by method returns a result if exists""" n_answers = 1 task = TaskFactory.create(n_answers=n_answers) task_run = TaskRunFactory.create(task=task) result = self.result_repo.get_by(project_id=1) err_msg = "There should be a result" assert result.project_id == 1, err_msg assert result.task_id == task.id, err_msg assert len(result.task_run_ids) == n_answers, err_msg err_msg = "The task_run id is missing in the results array" for tr_id in result.task_run_ids: assert tr_id == task_run.id, err_msg @with_context def test_get_returns_result_after_increasig_redundancy(self): """Test get method returns a result if after increasing redundancy""" n_answers = 1 task = TaskFactory.create(n_answers=n_answers) task_run = TaskRunFactory.create(task=task) result = self.result_repo.filter_by(project_id=1) err_msg = "There should be a result" assert len(result) == 1, err_msg result = result[0] assert result.project_id == 1, err_msg assert result.task_id == task.id, err_msg assert len(result.task_run_ids) == n_answers, err_msg err_msg = "The task_run id is missing in the results array" for tr_id in result.task_run_ids: assert tr_id == task_run.id, err_msg # Increase redundancy tmp = task_repo.get_task(task.id) tmp.n_answers = 2 task_repo.update(task) err_msg = "There should be only one result" results = result_repo.filter_by(project_id=1) assert len(results) == 1, err_msg task_run_2 = TaskRunFactory.create(task=task) err_msg = "There should be 1 results" results = result_repo.filter_by(project_id=1) assert len(results) == 1, err_msg err_msg = "There should be 2 results" results = result_repo.filter_by(project_id=1, last_version=False) assert len(results) == 2, err_msg assert results[1].project_id == 1, err_msg assert results[1].task_id == task.id, err_msg err_msg = "First result should have only one task run ID" assert len(results[0].task_run_ids) == 1, err_msg err_msg = "Second result should have only two task run IDs" assert len(results[1].task_run_ids) == 2, err_msg err_msg = "The task_run id is missing in the results array" for tr_id in results[1].task_run_ids: assert tr_id in [task_run.id, task_run_2.id], err_msg @with_context def test_get_returns_no_result(self): """Test get method does not return a result if task not completed""" n_answers = 3 task = TaskFactory.create(n_answers=n_answers) TaskRunFactory.create(task=task) result = self.result_repo.filter_by(project_id=1) err_msg = "There should not be a result" assert len(result) == 0, err_msg @with_context def test_fulltext_search_result(self): """Test fulltext search in JSON info works.""" result = self.create_result() text = 'something word you me bar' data = {'foo': text} result.info = data self.result_repo.update(result) info = 'foo::word' res = self.result_repo.filter_by(info=info, fulltextsearch='1') assert len(res) == 1, len(res) assert res[0][0].info['foo'] == text, res[0] res = self.result_repo.filter_by(info=info) assert len(res) == 0, len(res) @with_context def test_fulltext_search_result_01(self): """Test fulltext search in JSON info works.""" result = self.create_result() text = 'something word you me bar' data = {'foo': text, 'bar': 'foo'} result.info = data self.result_repo.update(result) info = 'foo::word&bar|bar::foo' res = self.result_repo.filter_by(info=info, fulltextsearch='1') assert len(res) == 1, len(res) assert res[0][0].info['foo'] == text, res[0] @with_context def test_info_json_search_result(self): """Test search in JSON info works.""" result = self.create_result() text = 'bar' data = {'foo': text} result.info = data self.result_repo.update(result) info = 'foo::bar' res = self.result_repo.filter_by(info=info) assert len(res) == 1, len(res) assert res[0].info['foo'] == text, res[0] @with_context def test_update(self): """Test update persists the changes made to the result""" result = self.create_result() result.info = dict(new='value') self.result_repo.update(result) updated_result = self.result_repo.get(result.id) assert updated_result.info['new'] == 'value', updated_result @with_context def test_update_fails_if_integrity_error(self): """Test update raises a DBIntegrityError if the instance to be updated lacks a required value""" result = self.create_result() result.project_id = None assert_raises(DBIntegrityError, self.result_repo.update, result) @with_context def test_update_only_updates_results(self): """Test update raises a WrongObjectError when an object which is not a Result instance is updated""" bad_object = dict() assert_raises(WrongObjectError, self.result_repo.update, bad_object) @with_context def test_delete_results_from_project(self): """Test delte_results_from_project works.""" project = ProjectFactory.create() task = TaskFactory.create(project=project,n_answers=1) taskrun = TaskRunFactory.create(task=task, project=project) result = result_repo.get_by(project_id=task.project.id) assert result result_repo.delete_results_from_project(project) result = result_repo.get_by(project_id=task.project.id) assert result is None
class TestResultAPI(TestAPI): def setUp(self): super(TestResultAPI, self).setUp() self.result_repo = ResultRepository(db) def create_result(self, n_results=1, n_answers=1, owner=None, filter_by=False): if owner: owner = owner else: owner = UserFactory.create() project = ProjectFactory.create(owner=owner) tasks = [] for i in range(n_results): tasks.append( TaskFactory.create(n_answers=n_answers, project=project)) for i in range(n_answers): for task in tasks: TaskRunFactory.create(task=task, project=project) if filter_by: return self.result_repo.filter_by(project_id=1) else: return self.result_repo.get_by(project_id=1) @with_context @patch('pybossa.api.task.TaskAPI._verify_auth') def test_result_query_without_params(self, auth): """ Test API Result query""" user = UserFactory.create() auth.return_value = True result = self.create_result(n_answers=10) res = self.app.get('/api/result?all=1&api_key=' + user.api_key) results = json.loads(res.data) assert len(results) == 1, results result = results[0] assert result['info'] is None, result assert len(result['task_run_ids']) == 10, result assert result['task_run_ids'] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], result assert result['project_id'] == 1, result assert result['task_id'] == 1, result assert result['created'] is not None, result # Related res = self.app.get('/api/result?related=True&all=1&api_key=' + user.api_key) results = json.loads(res.data) assert len(results) == 1, results result = results[0] assert result['info'] is None, result assert len(result['task_run_ids']) == 10, result assert result['task_run_ids'] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], result assert result['project_id'] == 1, result assert result['task_id'] == 1, result assert result['created'] is not None, result assert result['task']['id'] == result['task_id'], result assert len(result['task_runs']) == 10, result for tr in result['task_runs']: assert tr['task_id'] == result['task_id'], tr url = '/api/taskrun?id=%s&related=True&all=1&api_key=%s' % ( tr['id'], user.api_key) taskrun = self.app.get(url) taskrun = json.loads(taskrun.data)[0] assert taskrun['result']['id'] == result['id'], taskrun['result'] assert taskrun['task']['id'] == result['task_id'], taskrun['task'] url = '/api/task?&all=1&id=%s&related=True&api_key=%s' % ( result['task_id'], user.api_key) task = self.app.get(url) task = json.loads(task.data)[0] assert task['result']['id'] == result['id'], task['result'] for tr in task['task_runs']: assert tr['id'] in result['task_run_ids'], task['task'] result = self.create_result(n_answers=10) result = result_repo.get(2) result.created = '2119-01-01T14:37:30.642119' result_repo.update(result) url = '/api/result?orderby=created&desc=true&all=1&api_key=' + user.api_key res = self.app.get(url) data = json.loads(res.data) print data err_msg = "It should get the last item first." assert data[0]['created'] == '2119-01-01T14:37:30.642119', err_msg url = '/api/result?orderby=id&desc=false&all=1&api_key=' + user.api_key res = self.app.get(url) data = json.loads(res.data) err_msg = "It should be sorted by id." assert data[1]['id'] == result.id, err_msg url = '/api/result?orderby=wrongattribute&api_key=' + user.api_key res = self.app.get(url) data = json.loads(res.data) err_msg = "It should be 415." assert data['status'] == 'failed', data assert data['status_code'] == 415, data assert 'has no attribute' in data['exception_msg'], data url = '/api/result?&all=1&api_key=' + user.api_key res = self.app.get(url) data = json.loads(res.data) err_msg = "It should get not the last item first." assert data[0]['created'] != '2119-01-01T14:37:30.642119', err_msg # The output should have a mime-type: application/json assert res.mimetype == 'application/json', res @with_context def test_result_query_without_params_with_context(self): """ Test API Result query with context.""" user = UserFactory.create() result = self.create_result(n_answers=10) res = self.app.get('/api/result?&all=1&api_key=' + user.api_key) results = json.loads(res.data) assert len(results) == 1, results result = results[0] assert result['info'] is None, result assert len(result['task_run_ids']) == 10, result assert result['task_run_ids'] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], result assert result['project_id'] == 1, result assert result['task_id'] == 1, result assert result['created'] is not None, result # The output should have a mime-type: application/json assert res.mimetype == 'application/json', res @with_context def test_result_query_with_params(self): """Test API query for result with params works""" owner = UserFactory.create() results = self.create_result(n_results=10, filter_by=True, owner=owner) # Test for real field res = self.app.get("/api/result?api_key=" + owner.api_key) data = json.loads(res.data) # Should return one result assert len(data) == 10, data # Correct result assert data[0]['project_id'] == 1, data res = self.app.get("/api/project?api_key=" + owner.api_key) project = json.loads(res.data) assert len(project) == 1, project assert project[0]['owner_id'] == owner.id, project owner_two = UserFactory.create() res = self.app.get("/api/result?api_key=" + owner_two.api_key) data = json.loads(res.data) err_403 = "Regular user should be Forbidden from accessing results" assert res.status_code == 403, err_403 owner_two = UserFactory.create() res = self.app.get("/api/result?all=1&api_key=" + owner_two.api_key) data = json.loads(res.data) assert res.status_code == 403, err_403 # Valid field but wrong value url = "/api/result?project_id=99999999&api_key=" + owner.api_key res = self.app.get(url) data = json.loads(res.data) assert len(data) == 0, data # Multiple fields url = '/api/result?project_id=1&task_id=1&api_key=' + owner.api_key res = self.app.get(url) data = json.loads(res.data) # One result assert len(data) == 1, data # Correct result assert data[0]['project_id'] == 1, data assert data[0]['task_id'] == 1, data # Multiple fields url = '/api/result?project_id=1&task_id=1&api_key=' + owner_two.api_key res = self.app.get(url) assert res.status_code == 403, err_403 url = '/api/result?all=1&project_id=1&task_id=1&api_key=' + owner_two.api_key res = self.app.get(url) assert res.status_code == 403, err_403 # Limits url = "/api/result?project_id=1&limit=5&api_key=" + owner.api_key res = self.app.get(url) data = json.loads(res.data) for item in data: assert item['project_id'] == 1, item assert len(data) == 5, len(data) # Limits url = "/api/result?project_id=1&limit=5&api_key=" + owner_two.api_key res = self.app.get(url) assert res.status_code == 403, err_403 # Limits url = "/api/result?all=1&project_id=1&limit=5&api_key=" + owner_two.api_key res = self.app.get(url) assert res.status_code == 403, err_403 # Keyset pagination url = "/api/result?project_id=1&limit=5&last_id=1&api_key=" + owner.api_key res = self.app.get(url) data = json.loads(res.data) for item in data: assert item['project_id'] == 1, item assert len(data) == 5, data assert data[0]['id'] == 2, data[0] # Keyset pagination url = "/api/result?project_id=1&limit=5&last_id=1&api_key=" + owner_two.api_key res = self.app.get(url) assert res.status_code == 403, err_403 # Keyset pagination url = "/api/result?all=1&project_id=1&limit=5&last_id=1&api_key=" + owner.api_key res = self.app.get(url) data = json.loads(res.data) for item in data: assert item['project_id'] == 1, item assert len(data) == 5, data assert data[0]['id'] == 2, data[0] @with_context def test_result_post(self): """Test API Result creation""" admin = UserFactory.create() user = UserFactory.create() non_owner = UserFactory.create() project = ProjectFactory.create(owner=user) data = dict(info='final result') # anonymous user # no api-key res = self.app.post('/api/result', data=json.dumps(data)) error_msg = 'Should not be allowed to create' assert_equal(res.status, '401 UNAUTHORIZED', error_msg) ### real user but not allowed as not owner! res = self.app.post('/api/result?api_key=' + non_owner.api_key, data=json.dumps(data)) error_msg = 'Should not be able to post tasks for projects of others' assert_equal(res.status, '403 FORBIDDEN', error_msg) # now a real user res = self.app.post('/api/result?api_key=' + user.api_key, data=json.dumps(data)) assert_equal(res.status, '403 FORBIDDEN', error_msg) # now the root user res = self.app.post('/api/result?api_key=' + admin.api_key, data=json.dumps(data)) assert_equal(res.status, '400 BAD REQUEST', error_msg) # POST with not JSON data url = '/api/result?api_key=%s' % user.api_key res = self.app.post(url, data=data) err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'POST', err assert err['exception_cls'] == 'ValueError', err # POST with not allowed args res = self.app.post(url + '&foo=bar', data=json.dumps(data)) err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'POST', err assert err['exception_cls'] == 'AttributeError', err # POST with fake data data['wrongfield'] = 13 res = self.app.post(url, data=json.dumps(data)) err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'POST', err assert err['exception_cls'] == 'TypeError', err @with_context def test_result_post_with_reserved_fields_returns_error(self): user = UserFactory.create() project = ProjectFactory.create(owner=user) data = {'created': 'today', 'id': 222, 'project_id': project.id} res = self.app.post('/api/result?api_key=' + user.api_key, data=json.dumps(data)) assert res.status_code == 400, res.status_code error = json.loads(res.data) assert error['exception_msg'] == "Reserved keys in payload", error @with_context def test_result_put_with_reserved_fields_returns_error(self): user = UserFactory.create() result = self.create_result(owner=user) print result url = '/api/result/%s?api_key=%s' % (result.id, user.api_key) data = {'created': 'today', 'project_id': 1, 'id': 222} res = self.app.put(url, data=json.dumps(data)) assert res.status_code == 400, res.status_code error = json.loads(res.data) assert error['exception_msg'] == "Reserved keys in payload", error @with_context def test_result_update(self): """Test API result update""" admin = UserFactory.create() user = UserFactory.create() make_subadmin(user) non_owner = UserFactory.create() data = dict(info=dict(foo='bar')) datajson = json.dumps(data) result = self.create_result(owner=user) ## anonymous res = self.app.put('/api/result/%s' % result.id, data=datajson) assert_equal(res.status, '401 UNAUTHORIZED', res.status) ### real user but not allowed as not owner! url = '/api/result/%s?api_key=%s' % (result.id, non_owner.api_key) res = self.app.put(url, data=datajson) assert_equal(res.status, '403 FORBIDDEN', res.status) ### real user url = '/api/result/%s?api_key=%s' % (result.id, user.api_key) res = self.app.put(url, data=datajson) out = json.loads(res.data) assert_equal(res.status, '200 OK', res.data) assert_equal(result.info['foo'], data['info']['foo']) assert result.id == out['id'], out ### root data = dict(info=dict(foo='root')) datajson = json.dumps(data) res = self.app.put('/api/result/%s?api_key=%s' % (result.id, admin.api_key), data=datajson) assert_equal(res.status, '200 OK', res.status) assert_equal(result.info['foo'], data['info']['foo']) assert result.id == out['id'], out # PUT with not JSON data res = self.app.put(url, data=None) err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'PUT', err assert err['exception_cls'] == 'ValueError', err # PUT with not allowed args res = self.app.put(url + "&foo=bar", data=json.dumps(data)) err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'PUT', err assert err['exception_cls'] == 'AttributeError', err # PUT with fake data data['wrongfield'] = 13 res = self.app.put(url, data=json.dumps(data)) err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'PUT', err assert err['exception_cls'] == 'TypeError', err @with_context def test_result_delete(self): """Test API result delete""" admin = UserFactory.create() user = UserFactory.create() non_owner = UserFactory.create() result = self.create_result(owner=user) ## anonymous res = self.app.delete('/api/result/%s' % result.id) error_msg = 'Anonymous should not be allowed to update' assert_equal(res.status, '401 UNAUTHORIZED', error_msg) ### real user but not allowed as not owner! url = '/api/result/%s?api_key=%s' % (result.id, non_owner.api_key) res = self.app.delete(url) error_msg = 'Should not be able to update tasks of others' assert_equal(res.status, '403 FORBIDDEN', error_msg) #### real user # DELETE with not allowed args res = self.app.delete(url + "&foo=bar") err = json.loads(res.data) assert res.status_code == 415, err assert err['status'] == 'failed', err assert err['target'] == 'result', err assert err['action'] == 'DELETE', err assert err['exception_cls'] == 'AttributeError', err # DELETE returns 403 url = '/api/result/%s?api_key=%s' % (result.id, user.api_key) res = self.app.delete(url) assert_equal(res.status, '403 FORBIDDEN', res.data) #### root user url = '/api/result/%s?api_key=%s' % (result.id, admin.api_key) res = self.app.delete(url) assert_equal(res.status, '403 FORBIDDEN', res.data) @with_context def test_get_last_version(self): """Test API result returns always latest version.""" result = self.create_result() project = project_repo.get(result.project_id) task = task_repo.get_task(result.task_id) task.n_answers = 2 TaskRunFactory.create(task=task, project=project) result = result_repo.get_by(project_id=project.id) assert result.last_version is True, result.last_version result_id = result.id results = result_repo.filter_by(project_id=project.id, last_version=False) assert len(results) == 2, len(results) for r in results: if r.id == result_id: assert r.last_version is True, r.last_version else: assert r.last_version is False, r.last_version @with_context def test_result_admin_post(self): """Test API Result creation""" admin = UserFactory.create() user = UserFactory.create() project = ProjectFactory.create(owner=user) data = dict(info='final result') # now the root user res = self.app.post('/api/result?api_key=' + admin.api_key, data=json.dumps(data)) assert res.status == '400 BAD REQUEST', res.status assert json.loads(res.data)['exception_msg'] == 'Invalid task id' task = TaskFactory.create(project=project, n_answers=1) data['task_id'] = task.id res = self.app.post('/api/result?api_key=' + admin.api_key, data=json.dumps(data)) assert res.status == '400 BAD REQUEST', res.status assert json.loads(res.data)['exception_msg'] == 'Invalid task' taskrun = TaskRunFactory.create(task=task) res = self.app.post('/api/result?api_key=' + admin.api_key, data=json.dumps(data)) assert res.status == '400 BAD REQUEST', res.status assert json.loads( res.data)['exception_msg'] == 'Record is already present' from pybossa.core import result_repo result_repo.delete_results_from_project(project) res = self.app.post('/api/result?api_key=' + admin.api_key, data=json.dumps(data)) assert res.status == '200 OK', res.status res_data = json.loads(res.data) assert res_data['task_id'] == task.id assert res_data['project_id'] == project.id assert res_data['info'] == data['info'] assert res_data['last_version'] assert res_data['task_run_ids'] == [taskrun.id]
class TestBulkTaskIIIFEnhancedImport(Test): def setUp(self): super(TestBulkTaskIIIFEnhancedImport, self).setUp() self.result_repo = ResultRepository(db) self.manifest_uri = 'http://example.org/iiif/book1/manifest' self.canvas_id_base = 'http://example.org/iiif/book1/canvas/p{0}' self.img_id_base = 'http://example.org/images/book1-page{0}-img{1}' def create_manifest(self, canvases=1, images=1): manifest = { '@context': 'http://iiif.io/api/presentation/2/context.json', '@id': self.manifest_uri, '@type': 'sc:Manifest', 'label': 'Foo', 'sequences': [{ '@type': 'sc:Sequence', 'canvases': [] }] } for i in range(canvases): canvas = { '@id': self.canvas_id_base.format(i), '@type': 'sc:Canvas', 'label': 'Bar', 'height': 100, 'width': 100, 'images': [] } for j in range(images): image = { '@type': 'oa:Annotation', 'motivation': 'sc:painting', 'resource': { '@id': 'http://example.org/image{}.jpg'.format(j), '@type': 'dctypes:Image', 'service': { '@id': self.img_id_base.format(i, j) } }, 'on': 'http://example.org/{}'.format(i) } canvas['images'].append(image) manifest['sequences'][0]['canvases'].append(canvas) return manifest @with_context def test_bl_tasks_created_with_bl_link(self, requests): """Test that non-BL tasks are created with a non-BL link.""" manifest = self.create_manifest() headers = {'Content-Type': 'application/json'} response = FakeResponse(text=json.dumps(manifest), status_code=200, headers=headers, encoding='utf-8') requests.get.return_value = response importer = BulkTaskIIIFEnhancedImporter(manifest_uri=self.manifest_uri) tasks = importer.tasks() assert_equal(len(tasks), 1) link_query = '?manifest={}#?cv=0'.format(self.manifest_uri) link = 'http://universalviewer.io/uv.html' + link_query assert_equal(tasks[0]['info']['link'], link) @with_context def test_non_bl_tasks_created_with_non_bl_link(self, requests): """Test that non-BL tasks are created with a non-BL link.""" manifest = self.create_manifest() bl_manifest_id = 'https://api.bl.uk/metadata/iiif/id/manifest.json' manifest['@id'] = bl_manifest_id headers = {'Content-Type': 'application/json'} response = FakeResponse(text=json.dumps(manifest), status_code=200, headers=headers, encoding='utf-8') requests.get.return_value = response importer = BulkTaskIIIFEnhancedImporter(manifest_uri=bl_manifest_id) tasks = importer.tasks() assert_equal(len(tasks), 1) link = 'http://access.bl.uk/item/viewer/id#?cv=0' assert_equal(tasks[0]['info']['link'], link) @with_context def test_exeption_if_no_collection_iri_for_parent(self, requests): """Test exception if no collection iri when child tasks generated.""" manifest = self.create_manifest() headers = {'Content-Type': 'application/json'} response = FakeResponse(text=json.dumps(manifest), status_code=200, headers=headers, encoding='utf-8') requests.get.return_value = response parent = ProjectFactory() task = TaskFactory(project=parent, n_answers=1) TaskRunFactory.create(task=task) importer = BulkTaskIIIFEnhancedImporter(manifest_uri=self.manifest_uri, parent_id=parent.id) assert_raises(BulkImportException, importer.tasks) @with_context @patch('pybossa_lc.model.base.wa_client') def test_child_tasks_generated(self, mock_wa_client, requests): """Test that child tasks are generated.""" n_canvases = 3 n_images = 1 manifest = self.create_manifest(canvases=n_canvases, images=n_images) headers = {'Content-Type': 'application/json'} response = FakeResponse(text=json.dumps(manifest), status_code=200, headers=headers, encoding='utf-8') requests.get.return_value = response anno_fixtures = AnnotationFixtures() anno_collection_iri = 'example.org/annotations' category = CategoryFactory( info={'annotations': { 'results': anno_collection_iri }}) parent = ProjectFactory(category=category) tasks = TaskFactory.create_batch(n_canvases, project=parent, n_answers=1) # Create some annotations for each parent task expected = [] return_values = [] for i, task in enumerate(tasks): canvas_id = self.canvas_id_base.format(i) for j in range(n_images): TaskRunFactory.create(task=task) img_id = self.img_id_base.format(i, j) annotations = [ anno_fixtures.create(motivation='tagging', source=canvas_id), anno_fixtures.create(motivation='describing', source=canvas_id), anno_fixtures.create(motivation='commenting', source=canvas_id) ] result = self.result_repo.get_by(task_id=task.id) result.info = dict(annotations=anno_collection_iri) self.result_repo.update(result) return_values.append(annotations) # Store expected task data to check later link_query = '?manifest={}#?cv={}'.format(self.manifest_uri, i) link = 'http://universalviewer.io/uv.html' + link_query for anno in annotations[:2]: expected.append({ 'manifest': self.manifest_uri, 'target': anno['target'], 'link': link, 'tileSource': '{}/info.json'.format(img_id), 'url': '{}/full/max/0/default.jpg'.format(img_id), 'url_m': '{}/full/240,/0/default.jpg'.format(img_id), 'url_b': '{}/full/1024,/0/default.jpg'.format(img_id), 'parent_task_id': task.id }) mock_wa_client.search_annotations.side_effect = return_values importer = BulkTaskIIIFEnhancedImporter(manifest_uri=self.manifest_uri, parent_id=parent.id) tasks = importer.tasks() task_info = [task['info'] for task in tasks] expected = sorted(expected, key=lambda x: x['target']) assert_equal(task_info, expected) @with_context @patch('pybossa_lc.model.base.wa_client') def test_has_child_added_to_parent_results(self, mock_wa_client, requests): """Test that the has_children key is added to parent results.""" manifest = self.create_manifest() headers = {'Content-Type': 'application/json'} response = FakeResponse(text=json.dumps(manifest), status_code=200, headers=headers, encoding='utf-8') requests.get.return_value = response anno_collection_iri = 'example.org/annotations' # Create a task for each canvas n_tasks = 3 category = CategoryFactory( info={'annotations': { 'results': anno_collection_iri }}) parent = ProjectFactory(category=category) tasks = TaskFactory.create_batch(n_tasks, project=parent, n_answers=1) for task in tasks: TaskRunFactory.create(task=task) result = self.result_repo.get_by(task_id=task.id) result.info = dict(annotations=anno_collection_iri) self.result_repo.update(result) importer = BulkTaskIIIFEnhancedImporter(manifest_uri=self.manifest_uri, parent_id=parent.id) mock_wa_client.search_annotations.return_value = [] tasks = importer.tasks() results = self.result_repo.filter_by(project_id=parent.id) result_info = [result.info for result in results] expected = [{ 'annotations': anno_collection_iri, 'has_children': True }] * n_tasks assert_equal(result_info, expected)
class TestIIIFAnnotationAnalyst(Test): def setUp(self): super(TestIIIFAnnotationAnalyst, self).setUp() self.ctx = ContextFixtures() self.result_repo = ResultRepository(db) self.task_repo = TaskRepository(db) self.iiif_analyst = IIIFAnnotationAnalyst() self.comments = ['Some comment'] self.tags = { 'foo': [ dict(x=100, y=100, w=100, h=100), dict(x=200, y=200, w=200, h=200) ], 'bar': [dict(x=300, y=300, w=300, h=300)] } transcription_data = {'foo': ['bar', 'baz'], 'qux': ['quux', 'quuz']} self.transcriptions_df = pandas.DataFrame(transcription_data) self.comment_annos = [] for comment in self.comments: self.comment_annos.append({ 'motivation': 'commenting', 'body': { 'type': 'TextualBody', 'value': comment, 'purpose': 'commenting', 'format': 'text/plain' }, 'target': 'example.com' }) self.tagging_annos = [] for tag, rect_list in self.tags.items(): for rect in rect_list: self.tagging_annos.append({ 'motivation': 'tagging', 'body': { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }, 'target': { 'source': 'example.com', 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh={0},{1},{2},{3}'.format( rect['x'], rect['y'], rect['w'], rect['h']) } } }) self.transcription_annos = [] for tag, value_list in transcription_data.items(): for value in value_list: self.transcription_annos.append({ 'motivation': 'describing', 'body': [{ 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }, { 'type': 'TextualBody', 'purpose': 'describing', 'value': value, 'format': 'text/plain' }], 'target': 'example.com' }) self.data = { 'user_id': [1, 2, 3], 'info': [self.comment_annos, self.tagging_annos, self.transcription_annos] } def test_get_comments(self): """Test IIIF Annotation comments are returned.""" task_run_df = pandas.DataFrame(self.data) comments = self.iiif_analyst.get_comments(task_run_df) expected = [(1, comment) for comment in self.comments] assert_equal(comments, expected) def test_get_tags(self): """Test IIIF Annotation tags are returned.""" task_run_df = pandas.DataFrame(self.data) tags = self.iiif_analyst.get_tags(task_run_df) assert_dict_equal(tags, self.tags) def test_get_tags_with_body_list(self): """Test IIIF Annotation tags are returned when body is a list.""" self.tagging_annos[0]['body'] = [ self.tagging_annos[0]['body'], { 'type': 'TextualBody', 'purpose': 'classifying', 'value': 'foo' } ] task_run_df = pandas.DataFrame(self.data) tags = self.iiif_analyst.get_tags(task_run_df) assert_dict_equal(tags, self.tags) def test_get_transcriptions_df(self): """Test IIIF Annotation transcriptions are returned.""" task_run_df = pandas.DataFrame(self.data) df = self.iiif_analyst.get_transcriptions_df(task_run_df) assert_dict_equal(df.to_dict(), self.transcriptions_df.to_dict()) @with_context @patch('pybossa_lc.model.base.wa_client') def test_analysis_with_no_transcriptions(self, mock_client): """Test IIIF analysis with no transcriptions.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target) TaskRunFactory.create_batch(n_answers, task=task, info=[{ 'motivation': 'describing', 'body': [{ 'purpose': 'describing', 'value': '' }, { 'purpose': 'tagging', 'value': 'foo' }] }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) assert_equal(mock_client.create_annotation.called, False) @with_context @patch('pybossa_lc.model.base.wa_client') def test_fragment_selector_stripped(self, mock_client): """Test IIIF fragment selector is stripped if rule applied.""" n_answers = 3 source = 'example.com' target = { 'source': source, 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh=100,100,100,100' } } rules = dict(remove_fragment_selector=True) anno_collection = 'http://eg.com/collection' task = self.ctx.create_task(n_answers, target, rules=rules, anno_collection=anno_collection) tag = 'foo' value = 'bar' TaskRunFactory.create_batch(n_answers, task=task, info=[{ 'motivation': 'describing', 'body': [{ 'purpose': 'describing', 'value': value }, { 'purpose': 'tagging', 'value': 'foo' }] }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) func = mock_client.create_annotation func.assert_called_once_with( anno_collection, { 'motivation': 'describing', 'type': 'Annotation', 'generator': [{ "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" }], 'body': [{ 'type': 'TextualBody', 'purpose': 'describing', 'value': value, 'format': 'text/plain' }, { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }], 'target': source }) @with_context @patch('pybossa_lc.model.base.wa_client') def test_comment_annotation_created(self, mock_client): """Test IIIF comment annotations are created.""" n_answers = 1 target = 'example.com' anno_collection = 'http://eg.com/collection' task = self.ctx.create_task(n_answers, target, anno_collection=anno_collection) user = UserFactory() value = 'foo' TaskRunFactory.create_batch(n_answers, user=user, task=task, info=[{ 'motivation': 'commenting', 'body': { 'value': value } }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) func = mock_client.create_annotation func.assert_called_once_with( anno_collection, { 'motivation': 'commenting', 'type': 'Annotation', 'creator': { 'id': url_for('api.api_user', oid=user.id), 'type': 'Person', 'name': user.fullname, 'nickname': user.name }, 'generator': [{ "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" }], 'body': { 'type': 'TextualBody', 'purpose': 'commenting', 'value': value, 'format': 'text/plain' }, 'target': target }) @with_context @patch('pybossa_lc.model.base.wa_client') def test_transcriptions_are_normalised(self, mock_client): """Test IIIF transcriptions are normalised according to set rules.""" n_answers = 1 target = 'example.com' anno_collection = 'http://eg.com/collection' rules = dict(case='title', whitespace='full_stop', trim_punctuation=True) task = self.ctx.create_task(n_answers, target, rules=rules, anno_collection=anno_collection) tag = 'foo' values = ['HeLLo!', ' hello ', ' hELLO.'] for value in values: TaskRunFactory.create(task=task, info=[{ 'motivation': 'describing', 'body': [{ 'purpose': 'describing', 'value': value }, { 'purpose': 'tagging', 'value': tag }] }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) func = mock_client.create_annotation func.assert_called_once_with( anno_collection, { 'motivation': 'describing', 'type': 'Annotation', 'generator': [{ "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" }], 'body': [{ 'type': 'TextualBody', 'purpose': 'describing', 'value': 'Hello', 'format': 'text/plain' }, { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }], 'target': target }) @with_context @patch('pybossa_lc.model.base.wa_client') def test_with_matching_transcriptions(self, mock_client): """Test IIIF results with matching transcriptions.""" n_answers = 3 target = 'example.com' anno_collection = 'http://eg.com/collection' task = self.ctx.create_task(n_answers, target, rules={}, anno_collection=anno_collection) value = 'foo' tag = 'bar' TaskRunFactory.create_batch(n_answers, task=task, info=[{ 'motivation': 'describing', 'body': [{ 'purpose': 'describing', 'value': value }, { 'purpose': 'tagging', 'value': tag }] }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) func = mock_client.create_annotation func.assert_called_once_with( anno_collection, { 'motivation': 'describing', 'type': 'Annotation', 'generator': [{ "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" }], 'body': [{ 'type': 'TextualBody', 'purpose': 'describing', 'value': value, 'format': 'text/plain' }, { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }], 'target': target }) @with_context @patch('pybossa_lc.model.base.wa_client') def test_redundancy_increased_when_not_max(self, mock_client): """Test IIIF task redundancy is updated when max not reached.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target, max_answers=n_answers + 1) for i in range(n_answers): TaskRunFactory.create(task=task, info=[{ 'motivation': 'describing', 'body': [{ 'purpose': 'describing', 'value': i }, { 'purpose': 'tagging', 'value': i }] }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) assert_equal(mock_client.create_annotation.called, False) updated_task = self.task_repo.get_task(task.id) assert_equal(updated_task.n_answers, n_answers + 1) @with_context @patch('pybossa_lc.model.base.wa_client') def test_redundancy_not_increased_when_max(self, mock_client): """Test IIIF task redundancy is not updated when max is reached.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target, max_answers=n_answers) for i in range(n_answers): TaskRunFactory.create(task=task, info=[{ 'motivation': 'describing', 'body': [{ 'purpose': 'describing', 'value': i }, { 'purpose': 'tagging', 'value': i }] }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) assert_equal(mock_client.create_annotation.called, False) updated_task = self.task_repo.get_task(task.id) assert_equal(updated_task.n_answers, n_answers) @with_context @patch('pybossa_lc.model.base.wa_client') def test_redundancy_not_increased_for_tags(self, mock_client): """Test IIIF task redundancy is not updated for tags.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target, max_answers=n_answers + 1) for i in range(n_answers): TaskRunFactory.create( task=task, info=[{ 'motivation': 'tagging', 'body': { 'type': 'TextualBody', 'purpose': 'tagging', 'value': 'foo' }, 'target': { 'source': 'example.com', 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh={0},{0},{0},{0}'.format(i) } } }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) updated_task = self.task_repo.get_task(task.id) assert_equal(updated_task.n_answers, n_answers) @with_context @patch('pybossa_lc.model.base.wa_client') def test_redundancy_not_increased_for_comments(self, mock_client): """Test IIIF task redundancy is not updated for comments.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target, max_answers=n_answers + 1) for i in range(n_answers): TaskRunFactory.create(task=task, info=[{ 'motivation': 'commenting', 'body': { 'type': 'TextualBody', 'value': i, 'purpose': 'commenting', 'format': 'text/plain' } }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) updated_task = self.task_repo.get_task(task.id) assert_equal(updated_task.n_answers, n_answers) @with_context @patch('pybossa_lc.model.base.wa_client') def test_redundancy_not_increased_when_no_values(self, mock_client): """Test IIIF task redundancy is not updated when no values.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target, max_answers=n_answers + 1) for i in range(n_answers): TaskRunFactory.create(task=task, info=[{ 'motivation': 'describing', 'body': [{ 'purpose': 'describing', 'value': '' }, { 'purpose': 'tagging', 'value': 'foo' }] }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) updated_task = self.task_repo.get_task(task.id) assert_equal(updated_task.n_answers, n_answers) assert_equal(mock_client.create_annotation.called, False) @with_context @patch('pybossa_lc.model.base.wa_client') def test_equal_regions_combined(self, mock_client): """Test IIIF equal tag regions are combined.""" n_answers = 3 target = 'example.com' anno_collection = 'http://eg.com/collection' task = self.ctx.create_task(n_answers, target, rules={}, anno_collection=anno_collection) rect = dict(x=400, y=200, w=100, h=150) tag = 'foo' TaskRunFactory.create_batch( n_answers, task=task, info=[{ 'motivation': 'tagging', 'body': { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }, 'target': { 'source': 'example.com', 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh={0},{1},{2},{3}'.format(rect['x'], rect['y'], rect['w'], rect['h']) } } }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) func = mock_client.create_annotation func.assert_called_once_with( anno_collection, { 'motivation': 'tagging', 'type': 'Annotation', 'generator': [{ "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" }], 'body': { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }, 'target': { 'source': 'example.com', 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh={0},{1},{2},{3}'.format(rect['x'], rect['y'], rect['w'], rect['h']) } } }) @with_context @patch('pybossa_lc.model.base.wa_client') def test_equal_regions_combined(self, mock_client): """Test IIIF equal tag regions are combined.""" n_answers = 3 target = 'example.com' anno_collection = 'http://eg.com/collection' task = self.ctx.create_task(n_answers, target, rules={}, anno_collection=anno_collection) rect = dict(x=400, y=200, w=100, h=150) tag = 'foo' TaskRunFactory.create_batch( n_answers, task=task, info=[{ 'motivation': 'tagging', 'body': { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }, 'target': { 'source': 'example.com', 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh={0},{1},{2},{3}'.format(rect['x'], rect['y'], rect['w'], rect['h']) } } }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) func = mock_client.create_annotation func.assert_called_once_with( anno_collection, { 'motivation': 'tagging', 'type': 'Annotation', 'generator': [{ "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" }], 'body': { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }, 'target': { 'source': 'example.com', 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh={0},{1},{2},{3}'.format(rect['x'], rect['y'], rect['w'], rect['h']) } } }) @with_context @patch('pybossa_lc.model.base.wa_client') def test_similar_regions_combined(self, mock_client): """Test IIIF similar tag regions are combined.""" n_answers = 3 target = 'example.com' anno_collection = 'http://eg.com/collection' task = self.ctx.create_task(n_answers, target, rules={}, anno_collection=anno_collection) rect1 = dict(x=90, y=100, w=110, h=90) rect2 = dict(x=100, y=110, w=90, h=100) rect3 = dict(x=110, y=90, w=100, h=110) rects = [rect1, rect2, rect3] tag = 'foo' for rect in rects: TaskRunFactory.create( task=task, info=[{ 'motivation': 'tagging', 'body': { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }, 'target': { 'source': 'example.com', 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh={0},{1},{2},{3}'.format( rect['x'], rect['y'], rect['w'], rect['h']) } } }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) func = mock_client.create_annotation func.assert_called_once_with( anno_collection, { 'motivation': 'tagging', 'type': 'Annotation', 'generator': [{ "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" }], 'body': { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }, 'target': { 'source': 'example.com', 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh=90,90,120,110' } } }) @with_context @patch('pybossa_lc.model.base.wa_client') def test_different_regions_combined(self, mock_client): """Test IIIF different tag regions are not combined.""" n_answers = 3 target = 'example.com' anno_collection = 'http://eg.com/collection' task = self.ctx.create_task(n_answers, target, rules={}, anno_collection=anno_collection) rect1 = dict(x=10, y=10, w=10, h=10) rect2 = dict(x=100, y=100, w=100, h=100) rect3 = dict(x=200, y=200, w=200, h=200) rects = [rect1, rect2, rect3] tag = 'foo' for rect in rects: TaskRunFactory.create( task=task, info=[{ 'motivation': 'tagging', 'body': { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }, 'target': { 'source': 'example.com', 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh={0},{1},{2},{3}'.format( rect['x'], rect['y'], rect['w'], rect['h']) } } }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) assert_equal(mock_client.create_annotation.call_args_list, [ call( anno_collection, { 'motivation': 'tagging', 'type': 'Annotation', 'generator': [{ "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" }], 'body': { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }, 'target': { 'source': 'example.com', 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh={0},{1},{2},{3}'.format( rect1['x'], rect1['y'], rect1['w'], rect1['h']) } } }), call( anno_collection, { 'motivation': 'tagging', 'type': 'Annotation', 'generator': [ { "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" } ], 'body': { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }, 'target': { 'source': 'example.com', 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh={0},{1},{2},{3}'.format( rect2['x'], rect2['y'], rect2['w'], rect2['h']) } } }), call( anno_collection, { 'motivation': 'tagging', 'type': 'Annotation', 'generator': [ { "id": flask_app.config.get('GITHUB_REPO'), "type": "Software", "name": "LibCrowds", "homepage": flask_app.config.get('SPA_SERVER_NAME') }, { "id": url_for('api.api_task', oid=task.id), "type": "Software" } ], 'body': { 'type': 'TextualBody', 'purpose': 'tagging', 'value': tag }, 'target': { 'source': 'example.com', 'selector': { 'conformsTo': 'http://www.w3.org/TR/media-frags/', 'type': 'FragmentSelector', 'value': '?xywh={0},{1},{2},{3}'.format( rect3['x'], rect3['y'], rect3['w'], rect3['h']) } } }) ]) @with_context @patch('pybossa_lc.model.base.wa_client') def test_old_annotations_deleted(self, mock_client): """Test IIIF old Annotations deleted.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target) user = UserFactory() TaskRunFactory.create_batch(n_answers, user=user, task=task, info=[{ 'motivation': 'commenting', 'body': { 'value': 'foo' } }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_annos = [{'id': 'baz'}, {'id': 'qux'}] fake_search = MagicMock() fake_search.return_value = fake_annos mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id, analyse_full=True) mock_client.delete_batch.assert_called_once_with(fake_annos) @with_context @patch('pybossa_lc.model.base.wa_client') def test_results_with_annotations_not_analysed(self, mock_client): """Test results with Annotations already not analysed by default.""" n_answers = 3 target = 'example.com' task = self.ctx.create_task(n_answers, target) user = UserFactory() TaskRunFactory.create_batch(n_answers, user=user, task=task, info=[{ 'motivation': 'commenting', 'body': { 'value': 'foo' } }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_annos = [{'id': 'baz'}, {'id': 'qux'}] fake_search = MagicMock() fake_search.return_value = fake_annos mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) assert_equal(mock_client.delete_batch.called, False) @with_context @patch('pybossa_lc.model.base.wa_client') def test_annotation_collection_iri_added_to_result_info(self, mock_client): """Test IIIF result info updated with AnnotationCollection IRI.""" n_answers = 3 target = 'example.com' anno_collection = 'annotations.example.com' task = self.ctx.create_task(n_answers, target, anno_collection=anno_collection) user = UserFactory() TaskRunFactory.create_batch(n_answers, user=user, task=task, info=[{ 'motivation': 'commenting', 'body': { 'value': 'foo' } }]) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) assert_dict_equal(result.info, {'annotations': anno_collection}) @with_context @patch('pybossa_lc.model.base.wa_client') def test_task_rejected(self, mock_client): """Test IIIF annotation not created when task rejected * n_answers.""" n_answers = 3 target = 'example.com' anno_collection = 'annotations.example.com' task = self.ctx.create_task(n_answers, target, anno_collection=anno_collection) user = UserFactory() reason = 'invalid-task' TaskRunFactory.create_batch(n_answers, user=user, task=task, info={'reject': reason}) result = self.result_repo.filter_by(project_id=task.project_id)[0] fake_search = MagicMock() fake_search.return_value = [] mock_client.search_annotations = fake_search self.iiif_analyst.analyse(result.id) assert not mock_client.create_annotation.called assert_dict_equal(result.info, { 'annotations': anno_collection, 'rejected': reason })
class TestResultRepository(Test): def setUp(self): super(TestResultRepository, self).setUp() self.result_repo = ResultRepository(db) def create_result(self, n_answers=1, filter_by=False): task = TaskFactory.create(n_answers=n_answers) TaskRunFactory.create(task=task) if filter_by: return self.result_repo.filter_by(project_id=1) else: return self.result_repo.get_by(project_id=1) def test_get_return_none_if_no_result(self): """Test get method returns None if there is no result with the specified id""" result = self.result_repo.get(2) assert result is None, result def test_get_returns_result(self): """Test get method returns a result if exists""" n_answers = 1 task = TaskFactory.create(n_answers=n_answers) task_run = TaskRunFactory.create(task=task) result = self.result_repo.filter_by(project_id=1) err_msg = "There should be a result" assert len(result) == 1, err_msg result = result[0] assert result.project_id == 1, err_msg assert result.task_id == task.id, err_msg assert len(result.task_run_ids) == n_answers, err_msg err_msg = "The task_run id is missing in the results array" for tr_id in result.task_run_ids: assert tr_id == task_run.id, err_msg def test_get_by_returns_result(self): """Test get_by method returns a result if exists""" n_answers = 1 task = TaskFactory.create(n_answers=n_answers) task_run = TaskRunFactory.create(task=task) result = self.result_repo.get_by(project_id=1) err_msg = "There should be a result" assert result.project_id == 1, err_msg assert result.task_id == task.id, err_msg assert len(result.task_run_ids) == n_answers, err_msg err_msg = "The task_run id is missing in the results array" for tr_id in result.task_run_ids: assert tr_id == task_run.id, err_msg def test_get_returns_result_after_increasig_redundancy(self): """Test get method returns a result if after increasing redundancy""" n_answers = 1 task = TaskFactory.create(n_answers=n_answers) task_run = TaskRunFactory.create(task=task) result = self.result_repo.filter_by(project_id=1) err_msg = "There should be a result" assert len(result) == 1, err_msg result = result[0] assert result.project_id == 1, err_msg assert result.task_id == task.id, err_msg assert len(result.task_run_ids) == n_answers, err_msg err_msg = "The task_run id is missing in the results array" for tr_id in result.task_run_ids: assert tr_id == task_run.id, err_msg # Increase redundancy tmp = task_repo.get_task(task.id) tmp.n_answers = 2 task_repo.update(task) err_msg = "There should be only one result" results = result_repo.filter_by(project_id=1) assert len(results) == 1, err_msg task_run_2 = TaskRunFactory.create(task=task) err_msg = "There should be 1 results" results = result_repo.filter_by(project_id=1) assert len(results) == 1, err_msg err_msg = "There should be 2 results" results = result_repo.filter_by(project_id=1, last_version=False) assert len(results) == 2, err_msg assert results[1].project_id == 1, err_msg assert results[1].task_id == task.id, err_msg err_msg = "First result should have only one task run ID" assert len(results[0].task_run_ids) == 1, err_msg err_msg = "Second result should have only two task run IDs" assert len(results[1].task_run_ids) == 2, err_msg err_msg = "The task_run id is missing in the results array" for tr_id in results[1].task_run_ids: assert tr_id in [task_run.id, task_run_2.id], err_msg def test_get_returns_no_result(self): """Test get method does not return a result if task not completed""" n_answers = 3 task = TaskFactory.create(n_answers=n_answers) TaskRunFactory.create(task=task) result = self.result_repo.filter_by(project_id=1) err_msg = "There should not be a result" assert len(result) == 0, err_msg def test_update(self): """Test update persists the changes made to the result""" result = self.create_result() result.info = dict(new='value') self.result_repo.update(result) updated_result = self.result_repo.get(result.id) assert updated_result.info['new'] == 'value', updated_result def test_update_fails_if_integrity_error(self): """Test update raises a DBIntegrityError if the instance to be updated lacks a required value""" result = self.create_result() result.project_id = None assert_raises(DBIntegrityError, self.result_repo.update, result) def test_update_only_updates_results(self): """Test update raises a WrongObjectError when an object which is not a Result instance is updated""" bad_object = dict() assert_raises(WrongObjectError, self.result_repo.update, bad_object)