From 9761bdd1f2b0f72a2c0fa46b3dee1920a580a26a Mon Sep 17 00:00:00 2001 From: Georgios Andreadis Date: Tue, 7 Jul 2020 20:59:38 +0200 Subject: Implement portfolio endpoints --- web-server/main.py | 2 - web-server/opendc/api/v2/experiments/__init__.py | 0 .../api/v2/experiments/experimentId/__init__.py | 0 .../api/v2/experiments/experimentId/endpoint.py | 56 -------- .../v2/experiments/experimentId/test_endpoint.py | 133 ------------------ web-server/opendc/api/v2/paths.json | 6 +- web-server/opendc/api/v2/portfolios/__init__.py | 0 .../api/v2/portfolios/portfolioId/__init__.py | 0 .../api/v2/portfolios/portfolioId/endpoint.py | 65 +++++++++ .../api/v2/portfolios/portfolioId/test_endpoint.py | 149 +++++++++++++++++++++ web-server/opendc/api/v2/projects/endpoint.py | 2 +- .../opendc/api/v2/projects/projectId/endpoint.py | 6 +- .../v2/projects/projectId/experiments/__init__.py | 0 .../v2/projects/projectId/experiments/endpoint.py | 35 ----- .../projectId/experiments/test_endpoint.py | 78 ----------- .../v2/projects/projectId/portfolios/__init__.py | 0 .../v2/projects/projectId/portfolios/endpoint.py | 36 +++++ .../projects/projectId/portfolios/test_endpoint.py | 85 ++++++++++++ .../api/v2/projects/projectId/test_endpoint.py | 2 +- web-server/opendc/models/experiment.py | 24 ---- web-server/opendc/models/model.py | 8 +- web-server/opendc/models/portfolio.py | 24 ++++ 22 files changed, 374 insertions(+), 337 deletions(-) delete mode 100644 web-server/opendc/api/v2/experiments/__init__.py delete mode 100644 web-server/opendc/api/v2/experiments/experimentId/__init__.py delete mode 100644 web-server/opendc/api/v2/experiments/experimentId/endpoint.py delete mode 100644 web-server/opendc/api/v2/experiments/experimentId/test_endpoint.py create mode 100644 web-server/opendc/api/v2/portfolios/__init__.py create mode 100644 web-server/opendc/api/v2/portfolios/portfolioId/__init__.py create mode 100644 web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py create mode 100644 web-server/opendc/api/v2/portfolios/portfolioId/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/experiments/__init__.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/experiments/endpoint.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/experiments/test_endpoint.py create mode 100644 web-server/opendc/api/v2/projects/projectId/portfolios/__init__.py create mode 100644 web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py create mode 100644 web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py delete mode 100644 web-server/opendc/models/experiment.py create mode 100644 web-server/opendc/models/portfolio.py (limited to 'web-server') diff --git a/web-server/main.py b/web-server/main.py index af3e95b9..0d24958d 100644 --- a/web-server/main.py +++ b/web-server/main.py @@ -140,8 +140,6 @@ def serve_web_server_test(): @FLASK_CORE_APP.route('/') @FLASK_CORE_APP.route('/projects') @FLASK_CORE_APP.route('/projects/') -@FLASK_CORE_APP.route('/projects//experiments') -@FLASK_CORE_APP.route('/projects//experiments/') @FLASK_CORE_APP.route('/profile') def serve_index(project_id=None, experiment_id=None): return send_from_directory(STATIC_ROOT, 'index.html') diff --git a/web-server/opendc/api/v2/experiments/__init__.py b/web-server/opendc/api/v2/experiments/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/experiments/experimentId/__init__.py b/web-server/opendc/api/v2/experiments/experimentId/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/experiments/experimentId/endpoint.py b/web-server/opendc/api/v2/experiments/experimentId/endpoint.py deleted file mode 100644 index 6706dc57..00000000 --- a/web-server/opendc/api/v2/experiments/experimentId/endpoint.py +++ /dev/null @@ -1,56 +0,0 @@ -from opendc.models.experiment import Experiment -from opendc.models.project import Project -from opendc.util.rest import Response - - -def GET(request): - """Get this Experiment.""" - - request.check_required_parameters(path={'experimentId': 'string'}) - - experiment = Experiment.from_id(request.params_path['experimentId']) - - experiment.check_exists() - experiment.check_user_access(request.google_id, False) - - return Response(200, 'Successfully retrieved Experiment.', experiment.obj) - - -def PUT(request): - """Update this Experiments name.""" - - request.check_required_parameters(path={'experimentId': 'string'}, body={'experiment': { - 'name': 'string', - }}) - - experiment = Experiment.from_id(request.params_path['experimentId']) - - experiment.check_exists() - experiment.check_user_access(request.google_id, True) - - experiment.set_property('name', request.params_body['experiment']['name']) - - experiment.update() - - return Response(200, 'Successfully updated experiment.', experiment.obj) - - -def DELETE(request): - """Delete this Experiment.""" - - request.check_required_parameters(path={'experimentId': 'string'}) - - experiment = Experiment.from_id(request.params_path['experimentId']) - - experiment.check_exists() - experiment.check_user_access(request.google_id, True) - - project = Project.from_id(experiment.obj['projectId']) - project.check_exists() - if request.params_path['experimentId'] in project.obj['experimentIds']: - project.obj['experimentIds'].remove(request.params_path['experimentId']) - project.update() - - old_object = experiment.delete() - - return Response(200, 'Successfully deleted experiment.', old_object) diff --git a/web-server/opendc/api/v2/experiments/experimentId/test_endpoint.py b/web-server/opendc/api/v2/experiments/experimentId/test_endpoint.py deleted file mode 100644 index a284cf32..00000000 --- a/web-server/opendc/api/v2/experiments/experimentId/test_endpoint.py +++ /dev/null @@ -1,133 +0,0 @@ -from opendc.util.database import DB - - -def test_get_experiment_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get('/api/v2/experiments/1').status - - -def test_get_experiment_no_authorizations(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'projectId': '1', 'authorizations': []}) - res = client.get('/api/v2/experiments/1') - assert '403' in res.status - - -def test_get_experiment_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - 'projectId': '1', - '_id': '1', - 'authorizations': [{ - 'projectId': '2', - 'authorizationLevel': 'OWN' - }] - }) - res = client.get('/api/v2/experiments/1') - assert '403' in res.status - - -def test_get_experiment(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - 'projectId': '1', - '_id': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'EDIT' - }] - }) - res = client.get('/api/v2/experiments/1') - assert '200' in res.status - - -def test_update_experiment_missing_parameter(client): - assert '400' in client.put('/api/v2/experiments/1').status - - -def test_update_experiment_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put('/api/v2/experiments/1', json={ - 'experiment': { - 'name': 'test', - } - }).status - - -def test_update_experiment_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - assert '403' in client.put('/api/v2/experiments/1', json={ - 'experiment': { - 'name': 'test', - } - }).status - - -def test_update_experiment(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - - res = client.put('/api/v2/experiments/1', json={'experiment': { - 'name': 'test', - }}) - assert '200' in res.status - - -def test_delete_project_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete('/api/v2/experiments/1').status - - -def test_delete_project_different_user(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'googleId': 'other_test', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'delete_one', return_value=None) - assert '403' in client.delete('/api/v2/experiments/1').status - - -def test_delete_project(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'googleId': 'test', - 'experimentIds': ['1'], - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'delete_one', return_value={}) - mocker.patch.object(DB, 'update', return_value=None) - res = client.delete('/api/v2/experiments/1') - assert '200' in res.status diff --git a/web-server/opendc/api/v2/paths.json b/web-server/opendc/api/v2/paths.json index 01a7dcff..90d5a2e6 100644 --- a/web-server/opendc/api/v2/paths.json +++ b/web-server/opendc/api/v2/paths.json @@ -5,9 +5,11 @@ "/projects/{projectId}", "/projects/{projectId}/authorizations", "/projects/{projectId}/topologies", + "/projects/{projectId}/portfolios", "/topologies/{topologyId}", - "/projects/{projectId}/experiments", - "/experiments/{experimentId}", + "/portfolios/{portfolioId}", + "/portfolios/{portfolioId}/scenarios", + "/scenarios/{scenarioId}", "/schedulers", "/traces", "/traces/{traceId}", diff --git a/web-server/opendc/api/v2/portfolios/__init__.py b/web-server/opendc/api/v2/portfolios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/__init__.py b/web-server/opendc/api/v2/portfolios/portfolioId/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py new file mode 100644 index 00000000..0a50a526 --- /dev/null +++ b/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py @@ -0,0 +1,65 @@ +from opendc.models.portfolio import Portfolio +from opendc.models.project import Project +from opendc.util.rest import Response + + +def GET(request): + """Get this Portfolio.""" + + request.check_required_parameters(path={'portfolioId': 'string'}) + + portfolio = Portfolio.from_id(request.params_path['portfolioId']) + + portfolio.check_exists() + portfolio.check_user_access(request.google_id, False) + + return Response(200, 'Successfully retrieved portfolio.', portfolio.obj) + + +def PUT(request): + """Update this Portfolios name.""" + + request.check_required_parameters(path={'portfolioId': 'string'}, body={'portfolio': { + 'name': 'string', + 'targets': { + 'enabledMetrics': 'list', + 'repeatsPerScenario': 'int', + }, + }}) + + portfolio = Portfolio.from_id(request.params_path['portfolioId']) + + portfolio.check_exists() + portfolio.check_user_access(request.google_id, True) + + portfolio.set_property('name', + request.params_body['portfolio']['name']) + portfolio.set_property('targets.enabledMetrics', + request.params_body['portfolio']['targets']['enabledMetrics']) + portfolio.set_property('targets.repeatsPerScenario', + request.params_body['portfolio']['targets']['repeatsPerScenario']) + + portfolio.update() + + return Response(200, 'Successfully updated portfolio.', portfolio.obj) + + +def DELETE(request): + """Delete this Portfolio.""" + + request.check_required_parameters(path={'portfolioId': 'string'}) + + portfolio = Portfolio.from_id(request.params_path['portfolioId']) + + portfolio.check_exists() + portfolio.check_user_access(request.google_id, True) + + project = Project.from_id(portfolio.obj['projectId']) + project.check_exists() + if request.params_path['portfolioId'] in project.obj['portfolioIds']: + project.obj['portfolioIds'].remove(request.params_path['portfolioId']) + project.update() + + old_object = portfolio.delete() + + return Response(200, 'Successfully deleted portfolio.', old_object) diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/test_endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/test_endpoint.py new file mode 100644 index 00000000..7ac346d4 --- /dev/null +++ b/web-server/opendc/api/v2/portfolios/portfolioId/test_endpoint.py @@ -0,0 +1,149 @@ +from opendc.util.database import DB + + +def test_get_portfolio_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.get('/api/v2/portfolios/1').status + + +def test_get_portfolio_no_authorizations(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'projectId': '1', 'authorizations': []}) + res = client.get('/api/v2/portfolios/1') + assert '403' in res.status + + +def test_get_portfolio_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + 'projectId': '1', + '_id': '1', + 'authorizations': [{ + 'projectId': '2', + 'authorizationLevel': 'OWN' + }] + }) + res = client.get('/api/v2/portfolios/1') + assert '403' in res.status + + +def test_get_portfolio(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + 'projectId': '1', + '_id': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }] + }) + res = client.get('/api/v2/portfolios/1') + assert '200' in res.status + + +def test_update_portfolio_missing_parameter(client): + assert '400' in client.put('/api/v2/portfolios/1').status + + +def test_update_portfolio_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.put('/api/v2/portfolios/1', json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }).status + + +def test_update_portfolio_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + mocker.patch.object(DB, 'update', return_value={}) + assert '403' in client.put('/api/v2/portfolios/1', json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }).status + + +def test_update_portfolio(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }], + 'targets': { + 'enabledMetrics': [], + 'repeatsPerScenario': 1 + } + }) + mocker.patch.object(DB, 'update', return_value={}) + + res = client.put('/api/v2/portfolios/1', json={'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + }}) + assert '200' in res.status + + +def test_delete_project_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.delete('/api/v2/portfolios/1').status + + +def test_delete_project_different_user(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'googleId': 'other_test', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + mocker.patch.object(DB, 'delete_one', return_value=None) + assert '403' in client.delete('/api/v2/portfolios/1').status + + +def test_delete_project(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'googleId': 'test', + 'portfolioIds': ['1'], + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }] + }) + mocker.patch.object(DB, 'delete_one', return_value={}) + mocker.patch.object(DB, 'update', return_value=None) + res = client.delete('/api/v2/portfolios/1') + assert '200' in res.status diff --git a/web-server/opendc/api/v2/projects/endpoint.py b/web-server/opendc/api/v2/projects/endpoint.py index 10954cdd..bf031382 100644 --- a/web-server/opendc/api/v2/projects/endpoint.py +++ b/web-server/opendc/api/v2/projects/endpoint.py @@ -19,7 +19,7 @@ def POST(request): project.set_property('datetimeCreated', Database.datetime_to_string(datetime.now())) project.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) project.set_property('topologyIds', [topology.get_id()]) - project.set_property('experimentIds', []) + project.set_property('portfolioIds', []) project.insert() topology.set_property('projectId', project.get_id()) diff --git a/web-server/opendc/api/v2/projects/projectId/endpoint.py b/web-server/opendc/api/v2/projects/projectId/endpoint.py index 6f80f827..1a4c090a 100644 --- a/web-server/opendc/api/v2/projects/projectId/endpoint.py +++ b/web-server/opendc/api/v2/projects/projectId/endpoint.py @@ -1,6 +1,6 @@ from datetime import datetime -from opendc.models.experiment import Experiment +from opendc.models.portfolio import Portfolio from opendc.models.project import Project from opendc.models.topology import Topology from opendc.models.user import User @@ -52,8 +52,8 @@ def DELETE(request): topology = Topology.from_id(topology_id) topology.delete() - for experiment_id in project.obj['experimentIds']: - experiment = Experiment.from_id(experiment_id) + for experiment_id in project.obj['portfolioIds']: + experiment = Portfolio.from_id(experiment_id) experiment.delete() user = User.from_google_id(request.google_id) diff --git a/web-server/opendc/api/v2/projects/projectId/experiments/__init__.py b/web-server/opendc/api/v2/projects/projectId/experiments/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/projects/projectId/experiments/endpoint.py b/web-server/opendc/api/v2/projects/projectId/experiments/endpoint.py deleted file mode 100644 index 2e5b93df..00000000 --- a/web-server/opendc/api/v2/projects/projectId/experiments/endpoint.py +++ /dev/null @@ -1,35 +0,0 @@ -from opendc.models.experiment import Experiment -from opendc.models.project import Project -from opendc.util.rest import Response - - -def POST(request): - """Add a new Experiment for this Project.""" - - request.check_required_parameters(path={'projectId': 'string'}, - body={ - 'experiment': { - 'topologyId': 'string', - 'traceId': 'string', - 'schedulerName': 'string', - 'name': 'string', - } - }) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, True) - - experiment = Experiment(request.params_body['experiment']) - - experiment.set_property('projectId', request.params_path['projectId']) - experiment.set_property('state', 'QUEUED') - experiment.set_property('lastSimulatedTick', 0) - - experiment.insert() - - project.obj['experimentIds'].append(experiment.get_id()) - project.update() - - return Response(200, 'Successfully added Experiment.', experiment.obj) diff --git a/web-server/opendc/api/v2/projects/projectId/experiments/test_endpoint.py b/web-server/opendc/api/v2/projects/projectId/experiments/test_endpoint.py deleted file mode 100644 index 11b79154..00000000 --- a/web-server/opendc/api/v2/projects/projectId/experiments/test_endpoint.py +++ /dev/null @@ -1,78 +0,0 @@ -from opendc.util.database import DB - - -def test_add_experiment_missing_parameter(client): - assert '400' in client.post('/api/v2/projects/1/experiments').status - - -def test_add_experiment_non_existing_project(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.post('/api/v2/projects/1/experiments', - json={ - 'experiment': { - 'topologyId': '1', - 'traceId': '1', - 'schedulerName': 'default', - 'name': 'test', - } - }).status - - -def test_add_experiment_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - assert '403' in client.post('/api/v2/projects/1/experiments', - json={ - 'experiment': { - 'topologyId': '1', - 'traceId': '1', - 'schedulerName': 'default', - 'name': 'test', - } - }).status - - -def test_add_experiment(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'experimentIds': ['1'], - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'EDIT' - }] - }) - mocker.patch.object(DB, - 'insert', - return_value={ - '_id': '1', - 'topologyId': '1', - 'traceId': '1', - 'schedulerName': 'default', - 'name': 'test', - 'state': 'QUEUED', - 'lastSimulatedTick': 0, - }) - mocker.patch.object(DB, 'update', return_value=None) - res = client.post( - '/api/v2/projects/1/experiments', - json={'experiment': { - 'topologyId': '1', - 'traceId': '1', - 'schedulerName': 'default', - 'name': 'test', - }}) - assert 'topologyId' in res.json['content'] - assert 'state' in res.json['content'] - assert 'lastSimulatedTick' in res.json['content'] - assert '200' in res.status diff --git a/web-server/opendc/api/v2/projects/projectId/portfolios/__init__.py b/web-server/opendc/api/v2/projects/projectId/portfolios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py b/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py new file mode 100644 index 00000000..c51dee14 --- /dev/null +++ b/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py @@ -0,0 +1,36 @@ +from opendc.models.portfolio import Portfolio +from opendc.models.project import Project +from opendc.util.rest import Response + + +def POST(request): + """Add a new Portfolio for this Project.""" + + request.check_required_parameters(path={'projectId': 'string'}, + body={ + 'portfolio': { + 'name': 'string', + 'targets': { + 'enabledMetrics': 'list', + 'repeatsPerScenario': 'int', + }, + } + }) + + project = Project.from_id(request.params_path['projectId']) + + project.check_exists() + project.check_user_access(request.google_id, True) + + portfolio = Portfolio(request.params_body['portfolio']) + + portfolio.set_property('projectId', request.params_path['projectId']) + portfolio.set_property('scenarioIds', []) + portfolio.set_property('baseScenarioId', '-1') + + portfolio.insert() + + project.obj['portfolioIds'].append(portfolio.get_id()) + project.update() + + return Response(200, 'Successfully added Portfolio.', portfolio.obj) diff --git a/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py b/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py new file mode 100644 index 00000000..5b4d9043 --- /dev/null +++ b/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py @@ -0,0 +1,85 @@ +from opendc.util.database import DB + + +def test_add_portfolio_missing_parameter(client): + assert '400' in client.post('/api/v2/projects/1/portfolios').status + + +def test_add_portfolio_non_existing_project(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.post('/api/v2/projects/1/portfolios', + json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }).status + + +def test_add_portfolio_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + assert '403' in client.post('/api/v2/projects/1/portfolios', + json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }).status + + +def test_add_portfolio(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'portfolioIds': ['1'], + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }] + }) + mocker.patch.object(DB, + 'insert', + return_value={ + '_id': '1', + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + }, + 'projectId': '1', + 'scenarioIds': [], + 'baseScenarioId': '-1', + }) + mocker.patch.object(DB, 'update', return_value=None) + res = client.post( + '/api/v2/projects/1/portfolios', + json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }) + assert 'projectId' in res.json['content'] + assert 'scenarioIds' in res.json['content'] + assert 'baseScenarioId' in res.json['content'] + assert '200' in res.status diff --git a/web-server/opendc/api/v2/projects/projectId/test_endpoint.py b/web-server/opendc/api/v2/projects/projectId/test_endpoint.py index c7450b5a..7a862e8d 100644 --- a/web-server/opendc/api/v2/projects/projectId/test_endpoint.py +++ b/web-server/opendc/api/v2/projects/projectId/test_endpoint.py @@ -111,7 +111,7 @@ def test_delete_project(client, mocker): 'authorizationLevel': 'OWN' }], 'topologyIds': [], - 'experimentIds': [], + 'portfolioIds': [], }) mocker.patch.object(DB, 'update', return_value=None) mocker.patch.object(DB, 'delete_one', return_value={'googleId': 'test'}) diff --git a/web-server/opendc/models/experiment.py b/web-server/opendc/models/experiment.py deleted file mode 100644 index 46373b99..00000000 --- a/web-server/opendc/models/experiment.py +++ /dev/null @@ -1,24 +0,0 @@ -from opendc.models.model import Model -from opendc.models.user import User -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response - - -class Experiment(Model): - """Model representing a Experiment.""" - - collection_name = 'experiments' - - def check_user_access(self, google_id, edit_access): - """Raises an error if the user with given [google_id] has insufficient access. - - Checks access on the parent project. - - :param google_id: The Google ID of the user. - :param edit_access: True when edit access should be checked, otherwise view access. - """ - user = User.from_google_id(google_id) - authorizations = list( - filter(lambda x: str(x['projectId']) == str(self.obj['projectId']), user.obj['authorizations'])) - if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): - raise ClientError(Response(403, 'Forbidden from retrieving/editing experiment.')) diff --git a/web-server/opendc/models/model.py b/web-server/opendc/models/model.py index cab283c9..bcb833ae 100644 --- a/web-server/opendc/models/model.py +++ b/web-server/opendc/models/model.py @@ -33,8 +33,12 @@ class Model: raise ClientError(Response(404, 'Not found.')) def set_property(self, key, value): - """Sets the given property on the enclosed object.""" - self.obj[key] = value + """Sets the given property on the enclosed object, with support for simple nested access.""" + if '.' in key: + keys = key.split('.') + self.obj[keys[0]][keys[1]] = value + else: + self.obj[key] = value def insert(self): """Inserts the enclosed object and generates a UUID for it.""" diff --git a/web-server/opendc/models/portfolio.py b/web-server/opendc/models/portfolio.py new file mode 100644 index 00000000..32961b63 --- /dev/null +++ b/web-server/opendc/models/portfolio.py @@ -0,0 +1,24 @@ +from opendc.models.model import Model +from opendc.models.user import User +from opendc.util.exceptions import ClientError +from opendc.util.rest import Response + + +class Portfolio(Model): + """Model representing a Portfolio.""" + + collection_name = 'portfolios' + + def check_user_access(self, google_id, edit_access): + """Raises an error if the user with given [google_id] has insufficient access. + + Checks access on the parent project. + + :param google_id: The Google ID of the user. + :param edit_access: True when edit access should be checked, otherwise view access. + """ + user = User.from_google_id(google_id) + authorizations = list( + filter(lambda x: str(x['projectId']) == str(self.obj['projectId']), user.obj['authorizations'])) + if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): + raise ClientError(Response(403, 'Forbidden from retrieving/editing portfolio.')) -- cgit v1.2.3 From e2e9cec1d4836a4cba81874129b8da8a12c216f6 Mon Sep 17 00:00:00 2001 From: Georgios Andreadis Date: Wed, 8 Jul 2020 14:35:47 +0200 Subject: Implement scenario adding endpoint --- web-server/main.py | 2 +- .../portfolios/portfolioId/scenarios/__init__.py | 0 .../portfolios/portfolioId/scenarios/endpoint.py | 42 ++++++++ .../portfolioId/scenarios/test_endpoint.py | 119 +++++++++++++++++++++ .../opendc/api/v2/projects/projectId/endpoint.py | 6 +- web-server/opendc/models/scenario.py | 26 +++++ web-server/opendc/util/parameter_checker.py | 6 ++ 7 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 web-server/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py create mode 100644 web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py create mode 100644 web-server/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py create mode 100644 web-server/opendc/models/scenario.py (limited to 'web-server') diff --git a/web-server/main.py b/web-server/main.py index 0d24958d..c466c0f2 100644 --- a/web-server/main.py +++ b/web-server/main.py @@ -141,7 +141,7 @@ def serve_web_server_test(): @FLASK_CORE_APP.route('/projects') @FLASK_CORE_APP.route('/projects/') @FLASK_CORE_APP.route('/profile') -def serve_index(project_id=None, experiment_id=None): +def serve_index(project_id=None): return send_from_directory(STATIC_ROOT, 'index.html') diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py new file mode 100644 index 00000000..ab32aae2 --- /dev/null +++ b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py @@ -0,0 +1,42 @@ +from opendc.models.portfolio import Portfolio +from opendc.util.rest import Response + + +def POST(request): + """Add a new Scenario for this Portfolio.""" + + request.check_required_parameters(path={'portfolioId': 'string'}, + body={ + 'scenario': { + 'name': 'string', + 'trace': { + 'traceId': 'string', + 'loadSamplingFraction': 'float', + }, + 'topology': { + 'topologyId': 'string', + }, + 'operational': { + 'failuresEnabled': 'bool', + 'performanceInterferenceEnabled': 'bool', + 'schedulerName': 'string', + }, + } + }) + + portfolio = Portfolio.from_id(request.params_path['portfolioId']) + + portfolio.check_exists() + portfolio.check_user_access(request.google_id, True) + + scenario = Portfolio(request.params_body['scenario']) + + scenario.set_property('portfolioId', request.params_path['portfolioId']) + scenario.set_property('simulationState', 'QUEUED') + + scenario.insert() + + portfolio.obj['portfolioIds'].append(portfolio.get_id()) + portfolio.update() + + return Response(200, 'Successfully added Portfolio.', portfolio.obj) diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py new file mode 100644 index 00000000..8b55bab0 --- /dev/null +++ b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py @@ -0,0 +1,119 @@ +from opendc.util.database import DB + + +def test_add_scenario_missing_parameter(client): + assert '400' in client.post('/api/v2/portfolios/1/scenarios').status + + +def test_add_scenario_non_existing_portfolio(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.post('/api/v2/portfolios/1/scenarios', + json={ + 'scenario': { + 'name': 'test', + 'trace': { + 'traceId': '1', + 'loadSamplingFraction': 1.0, + }, + 'topology': { + 'topologyId': '1', + }, + 'operational': { + 'failuresEnabled': True, + 'performanceInterferenceEnabled': False, + 'schedulerName': 'DEFAULT', + }, + } + }).status + + +def test_add_scenario_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'portfolioId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + assert '403' in client.post('/api/v2/portfolios/1/scenarios', + json={ + 'scenario': { + 'name': 'test', + 'trace': { + 'traceId': '1', + 'loadSamplingFraction': 1.0, + }, + 'topology': { + 'topologyId': '1', + }, + 'operational': { + 'failuresEnabled': True, + 'performanceInterferenceEnabled': False, + 'schedulerName': 'DEFAULT', + }, + } + }).status + + +def test_add_scenario(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'portfolioId': '1', + 'portfolioIds': ['1'], + 'scenarioIds': ['1'], + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }], + 'simulationState': 'QUEUED', + }) + mocker.patch.object(DB, + 'insert', + return_value={ + '_id': '1', + 'name': 'test', + 'trace': { + 'traceId': '1', + 'loadSamplingFraction': 1.0, + }, + 'topology': { + 'topologyId': '1', + }, + 'operational': { + 'failuresEnabled': True, + 'performanceInterferenceEnabled': False, + 'schedulerName': 'DEFAULT', + }, + 'portfolioId': '1', + 'simulationState': 'QUEUED', + }) + mocker.patch.object(DB, 'update', return_value=None) + res = client.post( + '/api/v2/portfolios/1/scenarios', + json={ + 'scenario': { + 'name': 'test', + 'trace': { + 'traceId': '1', + 'loadSamplingFraction': 1.0, + }, + 'topology': { + 'topologyId': '1', + }, + 'operational': { + 'failuresEnabled': True, + 'performanceInterferenceEnabled': False, + 'schedulerName': 'DEFAULT', + }, + } + }) + assert 'portfolioId' in res.json['content'] + assert 'simulationState' in res.json['content'] + assert '200' in res.status diff --git a/web-server/opendc/api/v2/projects/projectId/endpoint.py b/web-server/opendc/api/v2/projects/projectId/endpoint.py index 1a4c090a..77b66d75 100644 --- a/web-server/opendc/api/v2/projects/projectId/endpoint.py +++ b/web-server/opendc/api/v2/projects/projectId/endpoint.py @@ -52,9 +52,9 @@ def DELETE(request): topology = Topology.from_id(topology_id) topology.delete() - for experiment_id in project.obj['portfolioIds']: - experiment = Portfolio.from_id(experiment_id) - experiment.delete() + for portfolio_id in project.obj['portfolioIds']: + portfolio = Portfolio.from_id(portfolio_id) + portfolio.delete() user = User.from_google_id(request.google_id) user.obj['authorizations'] = list( diff --git a/web-server/opendc/models/scenario.py b/web-server/opendc/models/scenario.py new file mode 100644 index 00000000..d7d959ca --- /dev/null +++ b/web-server/opendc/models/scenario.py @@ -0,0 +1,26 @@ +from opendc.models.model import Model +from opendc.models.portfolio import Portfolio +from opendc.models.user import User +from opendc.util.exceptions import ClientError +from opendc.util.rest import Response + + +class Scenario(Model): + """Model representing a Scenario.""" + + collection_name = 'scenarios' + + def check_user_access(self, google_id, edit_access): + """Raises an error if the user with given [google_id] has insufficient access. + + Checks access on the parent project. + + :param google_id: The Google ID of the user. + :param edit_access: True when edit access should be checked, otherwise view access. + """ + portfolio = Portfolio.from_id(self.obj['portfolioId']) + user = User.from_google_id(google_id) + authorizations = list( + filter(lambda x: str(x['projectId']) == str(portfolio.get_id()), user.obj['authorizations'])) + if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): + raise ClientError(Response(403, 'Forbidden from retrieving/editing scenario.')) diff --git a/web-server/opendc/util/parameter_checker.py b/web-server/opendc/util/parameter_checker.py index d37256e0..214dfa9d 100644 --- a/web-server/opendc/util/parameter_checker.py +++ b/web-server/opendc/util/parameter_checker.py @@ -49,6 +49,12 @@ def _incorrect_parameter(params_required, params_actual, parent=''): if param_required == 'int' and not isinstance(param_actual, int): return '{}.{}'.format(parent, param_name) + if param_required == 'float' and not isinstance(param_actual, float): + return '{}.{}'.format(parent, param_name) + + if param_required == 'bool' and not isinstance(param_actual, bool): + return '{}.{}'.format(parent, param_name) + if param_required == 'string' and not isinstance(param_actual, str) and not isinstance(param_actual, int): return '{}.{}'.format(parent, param_name) -- cgit v1.2.3 From 34427810d7e3c2a3d3257c3783f394d041a2c7d7 Mon Sep 17 00:00:00 2001 From: Georgios Andreadis Date: Wed, 8 Jul 2020 15:58:56 +0200 Subject: Improve code quality --- web-server/opendc/util/parameter_checker.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) (limited to 'web-server') diff --git a/web-server/opendc/util/parameter_checker.py b/web-server/opendc/util/parameter_checker.py index 214dfa9d..d1256009 100644 --- a/web-server/opendc/util/parameter_checker.py +++ b/web-server/opendc/util/parameter_checker.py @@ -46,20 +46,18 @@ def _incorrect_parameter(params_required, params_actual, parent=''): except: return '{}.{}'.format(parent, param_name) - if param_required == 'int' and not isinstance(param_actual, int): - return '{}.{}'.format(parent, param_name) - - if param_required == 'float' and not isinstance(param_actual, float): - return '{}.{}'.format(parent, param_name) - - if param_required == 'bool' and not isinstance(param_actual, bool): - return '{}.{}'.format(parent, param_name) - - if param_required == 'string' and not isinstance(param_actual, str) and not isinstance(param_actual, int): - return '{}.{}'.format(parent, param_name) - - if param_required.startswith('list') and not isinstance(param_actual, list): - return '{}.{}'.format(parent, param_name) + type_pairs = [ + ('int', (int,)), + ('float', (float,)), + ('bool', (bool,)), + ('string', (str, int)), + ('list', (list,)), + ] + + for str_type, actual_types in type_pairs: + if param_required == str_type and all(not isinstance(param_actual, t) + for t in actual_types): + return '{}.{}'.format(parent, param_name) return None -- cgit v1.2.3 From 94bcde012f1473f828f89a39addc8994114f9a58 Mon Sep 17 00:00:00 2001 From: Georgios Andreadis Date: Wed, 8 Jul 2020 16:18:20 +0200 Subject: Implement scenario routes --- .../api/v2/portfolios/portfolioId/endpoint.py | 2 +- web-server/opendc/api/v2/scenarios/__init__.py | 0 .../opendc/api/v2/scenarios/scenarioId/__init__.py | 0 .../opendc/api/v2/scenarios/scenarioId/endpoint.py | 57 +++++++++ .../api/v2/scenarios/scenarioId/test_endpoint.py | 140 +++++++++++++++++++++ 5 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 web-server/opendc/api/v2/scenarios/__init__.py create mode 100644 web-server/opendc/api/v2/scenarios/scenarioId/__init__.py create mode 100644 web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py create mode 100644 web-server/opendc/api/v2/scenarios/scenarioId/test_endpoint.py (limited to 'web-server') diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py index 0a50a526..c0ca64e0 100644 --- a/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py +++ b/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py @@ -17,7 +17,7 @@ def GET(request): def PUT(request): - """Update this Portfolios name.""" + """Update this Portfolio.""" request.check_required_parameters(path={'portfolioId': 'string'}, body={'portfolio': { 'name': 'string', diff --git a/web-server/opendc/api/v2/scenarios/__init__.py b/web-server/opendc/api/v2/scenarios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web-server/opendc/api/v2/scenarios/scenarioId/__init__.py b/web-server/opendc/api/v2/scenarios/scenarioId/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py b/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py new file mode 100644 index 00000000..1baa157a --- /dev/null +++ b/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py @@ -0,0 +1,57 @@ +from opendc.models.scenario import Scenario +from opendc.models.portfolio import Portfolio +from opendc.util.rest import Response + + +def GET(request): + """Get this Scenario.""" + + request.check_required_parameters(path={'scenarioId': 'string'}) + + scenario = Scenario.from_id(request.params_path['scenarioId']) + + scenario.check_exists() + scenario.check_user_access(request.google_id, False) + + return Response(200, 'Successfully retrieved scenario.', scenario.obj) + + +def PUT(request): + """Update this Scenarios name.""" + + request.check_required_parameters(path={'scenarioId': 'string'}, body={'scenario': { + 'name': 'string', + }}) + + scenario = Scenario.from_id(request.params_path['scenarioId']) + + scenario.check_exists() + scenario.check_user_access(request.google_id, True) + + scenario.set_property('name', + request.params_body['scenario']['name']) + + scenario.update() + + return Response(200, 'Successfully updated scenario.', scenario.obj) + + +def DELETE(request): + """Delete this Scenario.""" + + request.check_required_parameters(path={'scenarioId': 'string'}) + + scenario = Scenario.from_id(request.params_path['scenarioId']) + + scenario.check_exists() + scenario.check_user_access(request.google_id, True) + + portfolio = Portfolio.from_id(scenario.obj['portfolioId']) + portfolio.check_exists() + if request.params_path['scenarioId'] in portfolio.obj['scenarioIds']: + portfolio.obj['scenarioIds'].remove(request.params_path['scenarioId']) + portfolio.update() + + old_object = scenario.delete() + + return Response(200, 'Successfully deleted scenario.', old_object) diff --git a/web-server/opendc/api/v2/scenarios/scenarioId/test_endpoint.py b/web-server/opendc/api/v2/scenarios/scenarioId/test_endpoint.py new file mode 100644 index 00000000..09b7d0c0 --- /dev/null +++ b/web-server/opendc/api/v2/scenarios/scenarioId/test_endpoint.py @@ -0,0 +1,140 @@ +from opendc.util.database import DB + + +def test_get_scenario_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.get('/api/v2/scenarios/1').status + + +def test_get_scenario_no_authorizations(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={ + 'portfolioId': '1', + 'authorizations': [] + }) + res = client.get('/api/v2/scenarios/1') + assert '403' in res.status + + +def test_get_scenario_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + 'portfolioId': '1', + '_id': '1', + 'authorizations': [{ + 'projectId': '2', + 'authorizationLevel': 'OWN' + }] + }) + res = client.get('/api/v2/scenarios/1') + assert '403' in res.status + + +def test_get_scenario(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + 'portfolioId': '1', + '_id': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }] + }) + res = client.get('/api/v2/scenarios/1') + assert '200' in res.status + + +def test_update_scenario_missing_parameter(client): + assert '400' in client.put('/api/v2/scenarios/1').status + + +def test_update_scenario_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.put('/api/v2/scenarios/1', json={ + 'scenario': { + 'name': 'test', + } + }).status + + +def test_update_scenario_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'portfolioId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + mocker.patch.object(DB, 'update', return_value={}) + assert '403' in client.put('/api/v2/scenarios/1', json={ + 'scenario': { + 'name': 'test', + } + }).status + + +def test_update_scenario(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'portfolioId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }], + 'targets': { + 'enabledMetrics': [], + 'repeatsPerScenario': 1 + } + }) + mocker.patch.object(DB, 'update', return_value={}) + + res = client.put('/api/v2/scenarios/1', json={'scenario': { + 'name': 'test', + }}) + assert '200' in res.status + + +def test_delete_project_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.delete('/api/v2/scenarios/1').status + + +def test_delete_project_different_user(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'portfolioId': '1', + 'googleId': 'other_test', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + mocker.patch.object(DB, 'delete_one', return_value=None) + assert '403' in client.delete('/api/v2/scenarios/1').status + + +def test_delete_project(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'portfolioId': '1', + 'googleId': 'test', + 'scenarioIds': ['1'], + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }] + }) + mocker.patch.object(DB, 'delete_one', return_value={}) + mocker.patch.object(DB, 'update', return_value=None) + res = client.delete('/api/v2/scenarios/1') + assert '200' in res.status -- cgit v1.2.3 From 0a1579e749663e872a53e25fa9fd26e209a9e019 Mon Sep 17 00:00:00 2001 From: Georgios Andreadis Date: Wed, 8 Jul 2020 16:19:14 +0200 Subject: Fix layout --- web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'web-server') diff --git a/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py b/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py index 1baa157a..02d39063 100644 --- a/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py +++ b/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py @@ -29,7 +29,7 @@ def PUT(request): scenario.check_user_access(request.google_id, True) scenario.set_property('name', - request.params_body['scenario']['name']) + request.params_body['scenario']['name']) scenario.update() -- cgit v1.2.3