Skip to content

Commit

Permalink
RDISCROWD-5637 add project details endpoint (#814)
Browse files Browse the repository at this point in the history
* 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 <nsyed22@bloomberg.net>
  • Loading branch information
n00rsy and nsyed22 committed Feb 16, 2023
1 parent 459b72b commit 8dfb76a
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 1 deletion.
2 changes: 2 additions & 0 deletions pybossa/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')

Expand Down
69 changes: 69 additions & 0 deletions pybossa/api/project_details.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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
5 changes: 4 additions & 1 deletion settings_test.py.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand Down
226 changes: 226 additions & 0 deletions test/test_api/test_project_details_api.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
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

0 comments on commit 8dfb76a

Please sign in to comment.