From 8dfb76a2cbe67b344c013931ecdee9586fcc6f14 Mon Sep 17 00:00:00 2001 From: Noor Syed Date: Thu, 16 Feb 2023 13:29:51 -0500 Subject: [PATCH] RDISCROWD-5637 add project details endpoint (#814) * add boilerplate endpoint * implement API query * refactor based on team review * cleanup project_details * added tests * clean _create_json_response * CR updates --------- Co-authored-by: nsyed22 --- pybossa/api/__init__.py | 2 + pybossa/api/project_details.py | 69 +++++++ settings_test.py.tmpl | 5 +- test/test_api/test_project_details_api.py | 226 ++++++++++++++++++++++ 4 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 pybossa/api/project_details.py create mode 100644 test/test_api/test_project_details_api.py diff --git a/pybossa/api/__init__.py b/pybossa/api/__init__.py index 87fb8427a1..949f68ef98 100644 --- a/pybossa/api/__init__.py +++ b/pybossa/api/__init__.py @@ -75,6 +75,7 @@ fetch_lock_for_user, release_reserve_task_lock_by_id) from pybossa.jobs import send_mail from pybossa.api.project_by_name import ProjectByNameAPI +from pybossa.api.project_details import ProjectDetailsAPI from pybossa.api.pwd_manager import get_pwd_manager from pybossa.data_access import data_access_levels from pybossa.task_creator_helper import set_gold_answers @@ -159,6 +160,7 @@ def register_api(view, endpoint, url, pk='id', pk_type='int'): register_api(CompletedTaskAPI, 'api_completedtask', '/completedtask', pk='oid', pk_type='int') register_api(CompletedTaskRunAPI, 'api_completedtaskrun', '/completedtaskrun', pk='oid', pk_type='int') register_api(ProjectByNameAPI, 'api_projectbyname', '/projectbyname', pk='key', pk_type='string') +register_api(ProjectDetailsAPI, 'api_projectdetails', '/projectdetails', pk='oid', pk_type='int') register_api(PerformanceStatsAPI, 'api_performancestats', '/performancestats', pk='oid', pk_type='int') register_api(BulkTasksAPI, 'api_bulktasks', '/bulktasks', pk='oid', pk_type='int') diff --git a/pybossa/api/project_details.py b/pybossa/api/project_details.py new file mode 100644 index 0000000000..08855053bd --- /dev/null +++ b/pybossa/api/project_details.py @@ -0,0 +1,69 @@ +# -*- coding: utf8 -*- +# This file is part of PYBOSSA. +# +# Copyright (C) 2017 Scifabric LTD. +# +# PYBOSSA is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PYBOSSA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with PYBOSSA. If not, see . + +import json +from flask import request, abort +from flask_login import current_user +from werkzeug.exceptions import BadRequest, Unauthorized, Forbidden +from pybossa.api.api_base import APIBase +from pybossa.model.project import Project +from pybossa.model import DomainObject + +class ProjectDetailsAPI(APIBase): + """ + Class for retreiving details about projects. + + """ + __class__ = Project + + def _filter_query(self, repo_info, limit, offset, orderby): + if (len(request.args.keys()) == 0 or + (len(request.args.keys()) == 1 and "api_key" in request.args.keys())): + return [] + if (not current_user.is_authenticated or + (not current_user.admin and not current_user.subadmin)): + raise Unauthorized("User not authorized for request") + + return APIBase._filter_query(self, repo_info, limit, offset, orderby) + + def _create_json_response(self, query_result, oid): + if len(query_result) == 1 and query_result[0] is None: + raise abort(404) + items = [] + for result in query_result: + try: + item = result + datum = self._create_dict_from_model(item) + items.append(datum) + except Exception: # pragma: no cover + raise + if oid is not None: + self._sign_item(items[0]) + items = items[0] + return json.dumps(items) + + + def _select_attributes(self, data): + tmp = {} + tmp['id'] = data.get('id') + tmp['short_name'] = data.get('short_name') + tmp['product'] = data.get('info', {}).get('product') + tmp['subproduct'] = data.get('info', {}).get('subproduct') + tmp['created'] = data.get('created') + + return tmp diff --git a/settings_test.py.tmpl b/settings_test.py.tmpl index bcf8c3a92c..2231958b8a 100644 --- a/settings_test.py.tmpl +++ b/settings_test.py.tmpl @@ -126,7 +126,10 @@ BSSO_SETTINGS = { } AVATAR_ABSOLUTE = True SPAM = ['fake.com'] -PRODUCTS_SUBPRODUCTS = {'abc': ['def']} +PRODUCTS_SUBPRODUCTS = { + 'abc': ['def'], + 'test_product': ['test_subproduct1', 'test_subproduct2'] +} # Wizard Steps # 'step_name': { diff --git a/test/test_api/test_project_details_api.py b/test/test_api/test_project_details_api.py new file mode 100644 index 0000000000..1d3f710011 --- /dev/null +++ b/test/test_api/test_project_details_api.py @@ -0,0 +1,226 @@ +# -*- coding: utf8 -*- +# This file is part of PYBOSSA. +# +# Copyright (C) 2015 Scifabric LTD. +# +# PYBOSSA is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PYBOSSA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with PYBOSSA. If not, see . +import json +from unittest.mock import patch, call + +from nose.tools import assert_equal + +from pybossa.model.project import Project +from test import db, with_context +from test.factories import (ProjectFactory, UserFactory) +from test.test_api import TestAPI + + + +class TestProjectAPI(TestAPI): + + def setUp(self): + super(TestProjectAPI, self).setUp() + db.session.query(Project).delete() + + def setupProjects(self): + project = ProjectFactory.create( + updated='2015-01-01T14:37:30.642119', + short_name='test-app1', + info={ + 'total': 150, + 'task_presenter': 'foo', + 'data_classification': dict(input_data="L4 - public", output_data="L4 - public"), + 'product' : 'test_product', + 'subproduct': 'test_subproduct1' + }) + + projects = ProjectFactory.create_batch(5, + info={ + 'total': 150, + 'task_presenter': 'foo', + 'data_classification': dict(input_data="L4 - public", output_data="L4 - public"), + 'product' : 'test_product', + 'subproduct': 'test_subproduct2' + }) + return project + + @with_context + def test_project_details_user_not_logged_in(self): + """ Test should return 401 if the user is not logged in""" + project = self.setupProjects() + project_id = str(project.id) + + res = self.app.get('/api/projectdetails/' + project_id) + err = json.loads(res.data) + assert res.status_code == 401, err + assert err['status'] == 'failed', err + assert err['target'] == 'project', err + assert err['exception_cls'] == 'Unauthorized', err + assert err['action'] == 'GET', err + + @with_context + def test_project_details_user_worker(self): + """ Test API should return 401 if user is worker""" + admin = UserFactory.create(admin=True) + worker = UserFactory.create(admin=False, subadmin=False) + + project = self.setupProjects() + project_id = str(project.id) + + res = self.app.get('/api/projectdetails?id=' + project_id + '&api_key=' + worker.api_key + '&all=1') + err = json.loads(res.data) + assert res.status_code == 401, err + assert err['status'] == 'failed', err + assert err['target'] == 'project', err + assert err['exception_cls'] == 'Unauthorized', err + assert err['action'] == 'GET', err + + @with_context + def test_project_details_user_subadmin(self): + """ Test API should work if user is subadmin""" + admin = UserFactory.create(admin=True) + subadmin = UserFactory.create(admin=False, subadmin=True) + + project = self.setupProjects() + project_id = str(project.id) + + res = self.app.get('/api/projectdetails?id=' + project_id + '&api_key=' + subadmin.api_key + '&all=1') + data = json.loads(res.data) + assert res.status_code == 200, data + assert data[0]['product'] == 'test_product', data + assert data[0]['short_name'] == 'test-app1', data + + @with_context + def test_project_details_user_admin(self): + """ Test API should work if user is admin""" + admin = UserFactory.create(admin=True) + + project = self.setupProjects() + project_id = str(project.id) + + res = self.app.get('/api/projectdetails?id=' + project_id + '&api_key=' + admin.api_key + '&all=1') + data = json.loads(res.data) + assert res.status_code == 200, data + assert data[0]['product'] == 'test_product', data + assert data[0]['short_name'] == 'test-app1', data + + + @with_context + def test_project_details_get_by_id_1(self): + """ Test get by id when result exists""" + admin = UserFactory.create(admin=True) + project1 = self.setupProjects() + + # Test get by id + res = self.app.get('/api/projectdetails?id=' + str(project1.id) + '&api_key=' + admin.api_key + '&all=1') + data = json.loads(res.data) + assert res.status_code == 200, data + assert len(data) == 1, data + assert data[0]['product'] == 'test_product', data + assert data[0]['short_name'] == 'test-app1', data + + @with_context + def test_project_details_get_by_id_2(self): + """ Test get by id when result exists""" + admin = UserFactory.create(admin=True) + project1 = self.setupProjects() + + # Test get by id + res = self.app.get('/api/projectdetails/' + str(project1.id) + '?api_key=' + admin.api_key) + data = json.loads(res.data) + assert res.status_code == 200, data + assert data['product'] == 'test_product', data + assert data['short_name'] == 'test-app1', data + + @with_context + def test_project_details_get_by_product(self): + """ Test search by product when result exists""" + admin = UserFactory.create(admin=True) + project1 = self.setupProjects() + + # Test get by product + res = self.app.get('/api/projectdetails?info=product::' + project1.info['product'] + '&api_key=' + admin.api_key + '&all=1') + data = json.loads(res.data) + assert res.status_code == 200, data + assert len(data) == 6, data + assert data[0]['product'] == 'test_product', data + assert data[1]['product'] == 'test_product', data + + @with_context + def test_project_details_get_by_subproduct(self): + """ Test search by subproduct when result exists""" + admin = UserFactory.create(admin=True) + project1 = self.setupProjects() + + # Test get by subproduct + res = self.app.get('/api/projectdetails?info=subproduct::' + project1.info['subproduct'] + '&api_key=' + admin.api_key + '&all=1') + data = json.loads(res.data) + assert res.status_code == 200, data + assert len(data) == 1, data + assert data[0]['product'] == 'test_product', data + assert data[0]['short_name'] == 'test-app1', data + + @with_context + def test_project_details_no_params(self): + """ Test API project query when no search params""" + admin = UserFactory.create(admin=True) + project1 = self.setupProjects() + + # Test no params + res = self.app.get('/api/projectdetails?api_key=' + admin.api_key) + data = json.loads(res.data) + assert res.status_code == 200, data + assert len(data) == 0, data + + @with_context + def test_project_details_value_does_not_match(self): + """ Test API project query when search value does not match""" + admin = UserFactory.create(admin=True) + project1 = self.setupProjects() + + # Test value DNE + res = self.app.get('/api/projectdetails?id=' + '9999' + '&api_key=' + admin.api_key + '&all=1') + data = json.loads(res.data) + assert res.status_code == 200, data + assert len(data) == 0, data + + + @with_context + def test_project_details_param_does_not_exist(self): + """ Test API project query when search value does not match""" + admin = UserFactory.create(admin=True) + project1 = self.setupProjects() + + # Test bad param + res = self.app.get('/api/projectdetails?fakeparam=product::' + project1.info['product'] + '&api_key=' + admin.api_key + '&all=1') + err = json.loads(res.data) + assert res.status_code == 415, data + assert err['status'] == 'failed', err + assert err['target'] == 'project', err + assert err['exception_cls'] == 'AttributeError', err + assert err['action'] == 'GET', err + + @with_context + def test_project_details_multiple_params(self): + """ Test API project query when result exists""" + admin = UserFactory.create(admin=True) + project1 = self.setupProjects() + + # Test get by product and subproduct + res = self.app.get('/api/projectdetails?info=product::' + project1.info['product'] + '&info=subproduct::' + project1.info['subproduct'] + '&api_key=' + admin.api_key + '&all=1') + data = json.loads(res.data) + assert res.status_code == 200, data + assert data[0]['product'] == 'test_product', data + assert data[0]['subproduct'] == 'test_subproduct1', data +