diff --git a/pagure/api/__init__.py b/pagure/api/__init__.py index 7705d52..c079ec1 100644 --- a/pagure/api/__init__.py +++ b/pagure/api/__init__.py @@ -526,6 +526,9 @@ def api(): project.api_update_project_watchers ) api_get_project_options_doc = load_doc(project.api_get_project_options) + api_modify_project_options_doc = load_doc( + project.api_modify_project_options + ) issues = [] if pagure_config.get("ENABLE_TICKETS", True): @@ -611,6 +614,7 @@ def api(): api_commit_add_flag_doc, api_update_project_watchers_doc, api_get_project_options_doc, + api_modify_project_options_doc, ], issues=issues, requests=[ diff --git a/pagure/api/project.py b/pagure/api/project.py index 9ea05af..9babd65 100644 --- a/pagure/api/project.py +++ b/pagure/api/project.py @@ -1961,3 +1961,92 @@ def api_get_project_options(repo, username=None, namespace=None): raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK) return flask.jsonify({"settings": project.settings, "status": "ok"}) + + +def _check_value(value): + """ Convert the provided value into a boolean, an int or leave it as it. + """ + if str(value).lower() in ["true"]: + value = True + elif str(value).lower() in ["false"]: + value = True + elif str(value).isnumeric(): + value = int(value) + return value + + +@API.route("//options/update", methods=["POST"]) +@API.route("///options/update", methods=["POST"]) +@API.route("/fork///options/update", methods=["POST"]) +@API.route( + "/fork////options/update", methods=["POST"] +) +@api_login_required(acls=["modify_project"]) +@api_method +def api_modify_project_options(repo, username=None, namespace=None): + """ + Update project options + ---------------------- + Allow project admins to modify the options of a project. + + :: + + POST /api/0//options/update + POST /api/0///options/update + + :: + + POST /api/0/fork///options/update + POST /api/0/fork////options/update + + Input + ^^^^^ + + Simply specify the key/values you would like to set. Beware that if you + do not specify in the request values that have been changed before they + will go back to their default value. + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + 'message': 'Edited successfully settings of repo: test', + 'status': 'ok' + } + + """ + project = get_authorized_api_project( + flask.g.session, repo, namespace=namespace + ) + if not project: + raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT) + + if flask.g.token.project and project != flask.g.token.project: + raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK) + + settings = {} + for key in flask.request.form: + + settings[key] = _check_value(flask.request.form[key]) + + try: + message = pagure.lib.query.update_project_settings( + flask.g.session, + repo=project, + settings=settings, + user=flask.g.fas_user.username, + from_api=True, + ) + flask.g.session.commit() + except pagure.exceptions.PagureException as err: + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.ENOCODE, error=str(err) + ) + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) + + return flask.jsonify({"message": message, "status": "ok"}) diff --git a/pagure/lib/query.py b/pagure/lib/query.py index 03baa60..87b90b5 100644 --- a/pagure/lib/query.py +++ b/pagure/lib/query.py @@ -2119,8 +2119,15 @@ def edit_issue( return messages -def update_project_settings(session, repo, settings, user): - """ Update the settings of a project. """ +def update_project_settings(session, repo, settings, user, from_api=False): + """ Update the settings of a project. + + If from_api is true, all values that are not specified will be changed + back to their default value. + Otherwise, if from_api is False, all non-specified values are assumed + to be set to ``False`` or ``None``. + + """ user_obj = get_user(session, user) update = [] @@ -2146,9 +2153,12 @@ def update_project_settings(session, repo, settings, user): update.append(key) new_settings[key] = settings[key] else: - val = False - if key == "Web-hooks": - val = None + if from_api: + val = new_settings[key] + else: + val = False + if key == "Web-hooks": + val = None # Ensure the default value is different from what is stored. if new_settings[key] != val: diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py index ecf758a..00c97d2 100644 --- a/tests/test_pagure_flask_api_project.py +++ b/tests/test_pagure_flask_api_project.py @@ -3871,6 +3871,147 @@ class PagureFlaskApiProjectOptionsTests(tests.Modeltests): } ) + def test_api_modify_project_options_wrong_project(self): + """ Test accessing api_modify_project_options w/ an invalid project. + """ + + headers = {'Authorization': 'token aaabbbcccddd'} + output = self.app.post('/api/0/unknown/options/update', headers=headers) + self.assertEqual(output.status_code, 404) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual( + data, + {u'error': u'Project not found', u'error_code': u'ENOPROJECT'} + ) + + def test_api_modify_project_options_wo_header(self): + """ Test accessing api_modify_project_options w/o auth header. """ + + output = self.app.post('/api/0/test/options/update') + self.assertEqual(output.status_code, 401) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual( + data, + { + u'error': u'Invalid or expired token. Please visit ' + 'http://localhost.localdomain/settings#api-keys to get ' + 'or renew your API token.', + u'error_code': u'EINVALIDTOK' + } + ) + + def test_api_modify_project_options_no_data(self): + """ Test accessing api_modify_project_options w/ auth header. """ + + # check before + headers = {'Authorization': 'token aaabbbcccddd'} + output = self.app.get('/api/0/test/options', headers=headers) + self.assertEqual(output.status_code, 200) + before = json.loads(output.get_data(as_text=True)) + self.assertEqual( + before, + { + "settings": { + "Enforce_signed-off_commits_in_pull-request": False, + "Minimum_score_to_merge_pull-request": -1, + "Only_assignee_can_merge_pull-request": False, + "Web-hooks": None, + "always_merge": False, + "disable_non_fast-forward_merges": False, + "fedmsg_notifications": True, + "issue_tracker": True, + "issue_tracker_read_only": False, + "issues_default_to_private": False, + "notify_on_commit_flag": False, + "notify_on_pull-request_flag": False, + "open_metadata_access_to_all": False, + "project_documentation": False, + "pull_request_access_only": False, + "pull_requests": True, + "stomp_notifications": True + }, + "status": "ok" + } + ) + + # Do not update anything + data = {} + output = self.app.post( + '/api/0/test/options/update', headers=headers, data=data) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual( + data, + { + u'message': u'No settings to change', + u'status': u'ok' + } + ) + + # check after + headers = {'Authorization': 'token aaabbbcccddd'} + output = self.app.get('/api/0/test/options', headers=headers) + self.assertEqual(output.status_code, 200) + after = json.loads(output.get_data(as_text=True)) + self.assertEqual(after, before) + + def test_api_modify_project_options(self): + """ Test accessing api_modify_project_options w/ auth header. """ + + # check before + headers = {'Authorization': 'token aaabbbcccddd'} + output = self.app.get('/api/0/test/options', headers=headers) + self.assertEqual(output.status_code, 200) + before = json.loads(output.get_data(as_text=True)) + self.assertEqual( + before, + { + "settings": { + "Enforce_signed-off_commits_in_pull-request": False, + "Minimum_score_to_merge_pull-request": -1, + "Only_assignee_can_merge_pull-request": False, + "Web-hooks": None, + "always_merge": False, + "disable_non_fast-forward_merges": False, + "fedmsg_notifications": True, + "issue_tracker": True, + "issue_tracker_read_only": False, + "issues_default_to_private": False, + "notify_on_commit_flag": False, + "notify_on_pull-request_flag": False, + "open_metadata_access_to_all": False, + "project_documentation": False, + "pull_request_access_only": False, + "pull_requests": True, + "stomp_notifications": True + }, + "status": "ok" + } + ) + + # Update: `issues_default_to_private`. + data = {"issues_default_to_private": True} + output = self.app.post( + '/api/0/test/options/update', headers=headers, data=data) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual( + data, + { + u'message': u'Edited successfully settings of repo: test', + u'status': u'ok' + } + ) + + # check after + headers = {'Authorization': 'token aaabbbcccddd'} + output = self.app.get('/api/0/test/options', headers=headers) + self.assertEqual(output.status_code, 200) + after = json.loads(output.get_data(as_text=True)) + self.assertNotEqual(before, after) + before["settings"]["issues_default_to_private"] = True + self.assertEqual(after, before) + if __name__ == '__main__': unittest.main(verbosity=2)