From e8dc7af98cfb570887c198c0f6796622f549cb59 Mon Sep 17 00:00:00 2001 From: Kory Becker Date: Mon, 17 Jun 2024 16:27:45 -0400 Subject: [PATCH 1/6] Added /api/locks/{project_id} --- pybossa/api/__init__.py | 2 ++ pybossa/settings_upref_mdata.py.tmpl | 26 ++++++++++++----------- pybossa/view/projects.py | 1 - test/test_api/test_project_details_api.py | 14 ++++++++++++ 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/pybossa/api/__init__.py b/pybossa/api/__init__.py index 458abc88b3..9921b88a57 100644 --- a/pybossa/api/__init__.py +++ b/pybossa/api/__init__.py @@ -77,6 +77,7 @@ from pybossa.jobs import send_mail from pybossa.api.project_by_name import ProjectByNameAPI, project_name_to_oid from pybossa.api.project_details import ProjectDetailsAPI +from pybossa.api.project_locks import ProjectLocksAPI 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 @@ -165,6 +166,7 @@ def register_api(view, endpoint, url, pk='id', 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(ProjectLocksAPI, 'api_projectlocks', '/locks', 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/settings_upref_mdata.py.tmpl b/pybossa/settings_upref_mdata.py.tmpl index b7257ebb5c..ec74638ee3 100644 --- a/pybossa/settings_upref_mdata.py.tmpl +++ b/pybossa/settings_upref_mdata.py.tmpl @@ -58,7 +58,7 @@ def upref_languages(): return langs -country_to_country_code = { +country_name_to_country_code = { "Andorra": "AD", "United Arab Emirates": "AE", "Afghanistan": "AF", @@ -313,24 +313,23 @@ country_to_country_code = { "Zimbabwe": "ZW", } +country_code_to_country_name = dict((reversed(item) for item in country_name_to_country_code.items())) -country_code_to_country = dict((reversed(item) for item in country_to_country_code.items())) +def get_country_code_by_country_name(country): + return country_name_to_country_code.get(country) -def get_country_code_by_country(country): - return country_to_country_code.get(country) +def get_country_name_by_country_code(country_code): + return country_code_to_country_name.get(country_code) +def upref_country_names(): + return [(cn, cn) for cn in country_name_to_country_code.keys()] -def get_country_by_country_code(country_code): - return country_code_to_country.get(country_code) - +def upref_country_codes(): + return [(cc, cc) for cc in country_code_to_country_name.keys()] def upref_locations(): - cts = [] - for cc, cn in country_code_to_country.items(): - cts.append((cc,cc)) - cts.append((cn, cn)) - return sorted(cts) + return sorted(upref_country_names() + upref_country_codes()) def mdata_user_types(): @@ -381,10 +380,13 @@ def get_upref_mdata_choices(): upref_mdata_choices = dict( languages=upref_languages(), locations=upref_locations(), + country_codes=upref_country_codes(), + country_names=upref_country_names(), timezones=mdata_timezones(), user_types=mdata_user_types()) return upref_mdata_choices + def get_valid_user_preferences(): if not valid_user_preferences: upref_mdata_choices = get_upref_mdata_choices() diff --git a/pybossa/view/projects.py b/pybossa/view/projects.py index 9c8d582ac5..c0fd23dd63 100644 --- a/pybossa/view/projects.py +++ b/pybossa/view/projects.py @@ -4264,7 +4264,6 @@ def get_locked_tasks(project, task_id=None): @blueprint.route('//locks/', methods=['GET'], defaults={'task_id': ''}) @blueprint.route('//locks//', methods=['GET']) @login_required -@csrf.exempt @admin_or_subadmin_required def locks(short_name, task_id): """View locked task(s) for a project.""" diff --git a/test/test_api/test_project_details_api.py b/test/test_api/test_project_details_api.py index 1d3f710011..726211af36 100644 --- a/test/test_api/test_project_details_api.py +++ b/test/test_api/test_project_details_api.py @@ -224,3 +224,17 @@ def test_project_details_multiple_params(self): assert data[0]['product'] == 'test_product', data assert data[0]['subproduct'] == 'test_subproduct1', data + @with_context + def test_project_locks_get_by_id(self): + """ Test get locks by project id when project id exists""" + admin = UserFactory.create(admin=True) + project1 = self.setupProjects() + + # Test get by id + res = self.app.get('/api/locks?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 + assert data[0]['locks'] == '[]', data From 26d4af286d9fc401144a17d3b76c32c82c76b13f Mon Sep 17 00:00:00 2001 From: Kory Becker Date: Mon, 17 Jun 2024 16:29:29 -0400 Subject: [PATCH 2/6] Added project_locks.py --- pybossa/api/project_locks.py | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 pybossa/api/project_locks.py diff --git a/pybossa/api/project_locks.py b/pybossa/api/project_locks.py new file mode 100644 index 0000000000..757781b146 --- /dev/null +++ b/pybossa/api/project_locks.py @@ -0,0 +1,73 @@ +# -*- 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 +from pybossa.view.projects import project_by_shortname, get_locked_tasks + +class ProjectLocksAPI(APIBase): + """ + Class for retreiving active locks in 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): + # Get the project. + project, owner, ps = project_by_shortname(data.get('short_name')) + task_id = '' + + tmp = {} + tmp['id'] = data.get('id') + tmp['short_name'] = data.get('short_name') + tmp['created'] = data.get('created') + tmp['locks'] = get_locked_tasks(project, task_id) + + return tmp From 6c34dfc8702b0b0a7c7c28782cba2dd87a6351a5 Mon Sep 17 00:00:00 2001 From: Kory Becker Date: Mon, 17 Jun 2024 16:51:58 -0400 Subject: [PATCH 3/6] fix --- test/test_api/test_project_details_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_api/test_project_details_api.py b/test/test_api/test_project_details_api.py index 726211af36..b761cac927 100644 --- a/test/test_api/test_project_details_api.py +++ b/test/test_api/test_project_details_api.py @@ -235,6 +235,5 @@ def test_project_locks_get_by_id(self): 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 assert data[0]['locks'] == '[]', data From 49db4dfa653c6ed16c162b584486e448ce559686 Mon Sep 17 00:00:00 2001 From: Kory Becker Date: Mon, 17 Jun 2024 17:48:31 -0400 Subject: [PATCH 4/6] unit test --- test/test_api/test_project_details_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_api/test_project_details_api.py b/test/test_api/test_project_details_api.py index b761cac927..13cc1a3d28 100644 --- a/test/test_api/test_project_details_api.py +++ b/test/test_api/test_project_details_api.py @@ -236,4 +236,4 @@ def test_project_locks_get_by_id(self): assert res.status_code == 200, data assert len(data) == 1, data assert data[0]['short_name'] == 'test-app1', data - assert data[0]['locks'] == '[]', data + assert data[0]['locks'] == [], data From 7e53960c889c35b34d8eeb67e215987d95abd440 Mon Sep 17 00:00:00 2001 From: Kory Becker Date: Tue, 18 Jun 2024 09:49:43 -0400 Subject: [PATCH 5/6] unit tests --- test/test_api/test_project_details_api.py | 14 -- test/test_api/test_project_locks.py | 171 ++++++++++++++++++++++ 2 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 test/test_api/test_project_locks.py diff --git a/test/test_api/test_project_details_api.py b/test/test_api/test_project_details_api.py index 13cc1a3d28..f959a89174 100644 --- a/test/test_api/test_project_details_api.py +++ b/test/test_api/test_project_details_api.py @@ -223,17 +223,3 @@ def test_project_details_multiple_params(self): assert res.status_code == 200, data assert data[0]['product'] == 'test_product', data assert data[0]['subproduct'] == 'test_subproduct1', data - - @with_context - def test_project_locks_get_by_id(self): - """ Test get locks by project id when project id exists""" - admin = UserFactory.create(admin=True) - project1 = self.setupProjects() - - # Test get by id - res = self.app.get('/api/locks?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]['short_name'] == 'test-app1', data - assert data[0]['locks'] == [], data diff --git a/test/test_api/test_project_locks.py b/test/test_api/test_project_locks.py new file mode 100644 index 0000000000..f74caf503a --- /dev/null +++ b/test/test_api/test_project_locks.py @@ -0,0 +1,171 @@ +# -*- 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_locks_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/locks/' + 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_locks_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/locks?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_locks_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/locks?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_locks_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/locks?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_locks_get_by_id(self): + """ Test get locks by project id when project id exists""" + admin = UserFactory.create(admin=True) + project1 = self.setupProjects() + + # Test get by id + res = self.app.get('/api/locks?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]['short_name'] == 'test-app1', data + assert data[0]['locks'] == [], data + + @with_context + def test_project_locks_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/locks?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_locks_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/locks?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_locks_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/locks?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 + From b047a86a3b196548cc60d1ae6829fb09a98126bc Mon Sep 17 00:00:00 2001 From: Kory Becker Date: Tue, 18 Jun 2024 10:12:06 -0400 Subject: [PATCH 6/6] unit tests --- test/test_api/test_project_locks.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_api/test_project_locks.py b/test/test_api/test_project_locks.py index f74caf503a..edaa8d3978 100644 --- a/test/test_api/test_project_locks.py +++ b/test/test_api/test_project_locks.py @@ -98,7 +98,6 @@ def test_project_locks_user_subadmin(self): res = self.app.get('/api/locks?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 @@ -112,7 +111,6 @@ def test_project_locks_user_admin(self): res = self.app.get('/api/locks?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