From 4875f7ae49de348145bbf6b690d5285a151d51da Mon Sep 17 00:00:00 2001 From: Nathan Hertz <nhertz@nrao.edu> Date: Wed, 17 Feb 2021 16:30:23 -0500 Subject: [PATCH] Added two new Pyramid views: `enable_capability` and `disable_capability` --- .../57c38b5f012e_capabilities_init.py | 1 + services/capability/src/capability/routes.py | 5 +- .../src/capability/views/capability.py | 38 +++++++++- services/capability/test/conftest.py | 6 +- .../test/test_capability_placeholder.py | 2 - .../capability/test/test_capability_server.py | 3 +- .../capability/test/test_capability_views.py | 74 ++++++++++++++++++- .../workspaces/capability/schema.py | 6 +- .../capability/services/capability_info.py | 13 +++- 9 files changed, 135 insertions(+), 13 deletions(-) delete mode 100644 services/capability/test/test_capability_placeholder.py diff --git a/schema/versions/57c38b5f012e_capabilities_init.py b/schema/versions/57c38b5f012e_capabilities_init.py index e550d9dbe..bd8ddb4bb 100644 --- a/schema/versions/57c38b5f012e_capabilities_init.py +++ b/schema/versions/57c38b5f012e_capabilities_init.py @@ -22,6 +22,7 @@ def upgrade(): sa.Column("capability_name", sa.String, primary_key=True), sa.Column("capability_steps", sa.String), sa.Column("max_jobs", sa.Integer), + sa.Column("enabled", sa.Boolean, default=True, server_default="true"), ) op.create_table( diff --git a/services/capability/src/capability/routes.py b/services/capability/src/capability/routes.py index 98d471798..ce095dc65 100644 --- a/services/capability/src/capability/routes.py +++ b/services/capability/src/capability/routes.py @@ -42,7 +42,10 @@ def capability_routes(config: Configurator): request_method="POST", ) config.add_route( - name="deactivate_capability", pattern=f"{capability_url}/deactivate", request_method="POST" + name="enable_capability", pattern=f"{capability_url}/enable", request_method="POST" + ) + config.add_route( + name="disable_capability", pattern=f"{capability_url}/disable", request_method="POST" ) diff --git a/services/capability/src/capability/views/capability.py b/services/capability/src/capability/views/capability.py index 9e97c4c90..14638b499 100644 --- a/services/capability/src/capability/views/capability.py +++ b/services/capability/src/capability/views/capability.py @@ -42,6 +42,7 @@ def create_capability(request: Request) -> Response: URL: capability/create :param request: POST request, expecting JSON parameters ["capability_name", "steps", "max_jobs"] + optionally, accepts a boolean "enabled" parameter :return: Response with JSON-formatted capability info of newly created capability or 400 response (HTTPBadRequest) if expected parameters not given or 412 response (HTTPPreconditionFailed) if capability with given name already exists @@ -67,6 +68,7 @@ def create_capability(request: Request) -> Response: name=params["capability_name"], steps=params["steps"], max_jobs=params["max_jobs"], + enabled=params.get("enabled", True), ) request.capability_info.save_entity(new_capability) return Response(json_body=new_capability.__json__()) @@ -98,11 +100,11 @@ def edit_capability(request: Request) -> Response: return HTTPBadRequest(detail=params_not_given_msg) elif not request.capability_info.lookup_capability(capability_name): # Capability with given name does not exist - already_exists_msg = ( + does_not_exist_msg = ( f"Capability {capability_name} does not exist.", f"To instead create a new capability, use capability/create with the same parameters.", ) - return HTTPPreconditionFailed(detail=already_exists_msg) + return HTTPPreconditionFailed(detail=does_not_exist_msg) else: steps = params.get("steps", None) max_jobs = params.get("max_jobs", None) @@ -111,3 +113,35 @@ def edit_capability(request: Request) -> Response: return Response(body="Capability successfully edited!") else: return HTTPExpectationFailed(detail=f"Unable to edit capability {capability_name}.") + + +@view_config(route_name="enable_capability", renderer="json") +def enable_capability(request: Request) -> Response: + capability_name = request.matchdict["capability_name"] + + if not request.capability_info.lookup_capability(capability_name): + # Capability with given name does not exist + does_not_exist_msg = f"Capability {capability_name} does not exist. Cannot enable." + return HTTPPreconditionFailed(detail=does_not_exist_msg) + else: + success = request.capability_info.edit_capability(capability_name, enabled=True) + if success: + return Response(body="Capability successfully enabled!") + else: + return HTTPExpectationFailed(detail=f"Unable to enable capability {capability_name}.") + + +@view_config(route_name="disable_capability", renderer="json") +def disable_capability(request: Request) -> Response: + capability_name = request.matchdict["capability_name"] + + if not request.capability_info.lookup_capability(capability_name): + # Capability with given name does not exist + does_not_exist_msg = f"Capability {capability_name} does not exist. Cannot disable." + return HTTPPreconditionFailed(detail=does_not_exist_msg) + else: + success = request.capability_info.edit_capability(capability_name, enabled=False) + if success: + return Response(body="Capability successfully disabled!") + else: + return HTTPExpectationFailed(detail=f"Unable to disable capability {capability_name}.") diff --git a/services/capability/test/conftest.py b/services/capability/test/conftest.py index 1609c8e7f..9731368c8 100644 --- a/services/capability/test/conftest.py +++ b/services/capability/test/conftest.py @@ -47,7 +47,9 @@ def request_null_capability() -> DummyRequest: ), ] - def edit_capability(self, name: str, steps: str = None, max_jobs: int = None) -> bool: + def edit_capability( + self, name: str, steps: str = None, max_jobs: int = None, enabled: bool = None + ) -> bool: if name == "error": # This is here to mimic the case where an update fails to happen return False @@ -57,6 +59,8 @@ def request_null_capability() -> DummyRequest: capability.steps = steps if max_jobs: capability.max_jobs = max_jobs + if enabled: + capability.enabled = enabled return True return False diff --git a/services/capability/test/test_capability_placeholder.py b/services/capability/test/test_capability_placeholder.py deleted file mode 100644 index 45b2bd089..000000000 --- a/services/capability/test/test_capability_placeholder.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_placeholder(): - pass \ No newline at end of file diff --git a/services/capability/test/test_capability_server.py b/services/capability/test/test_capability_server.py index a4f1c1c88..348033447 100644 --- a/services/capability/test/test_capability_server.py +++ b/services/capability/test/test_capability_server.py @@ -14,7 +14,8 @@ def capability_routes() -> RouteList: "view_capability", "create_capability", "edit_capability", - "deactivate_capability", + "enable_capability", + "disable_capability", "view_capability_request", "create_capability_request", "edit_capability_request", diff --git a/services/capability/test/test_capability_views.py b/services/capability/test/test_capability_views.py index bc3d4b174..9fa8feab6 100644 --- a/services/capability/test/test_capability_views.py +++ b/services/capability/test/test_capability_views.py @@ -18,7 +18,7 @@ def test_view_capability(test_config: Configurator, request_null_capability: Dum """ from capability.views.capability import view_capability - expected_response = '{"name": "null", "max_jobs": 2, "steps": "test"}' + expected_response = '{"name": "null", "max_jobs": 2, "steps": "test", "enabled": null}' request_null_capability.matchdict["capability_name"] = "null" response = view_capability(request_null_capability) assert response.status_code == 200 @@ -65,7 +65,7 @@ def test_create_capability(test_config: Configurator, request_null_capability: D response = create_capability(request_null_capability) assert response.status_code == 200 - expected_response = '{"name": "test_create", "max_jobs": 1, "steps": "test"}' + expected_response = '{"name": "test_create", "max_jobs": 1, "steps": "test", "enabled": true}' assert response.json_body == expected_response # Assert test capability has been added to list of capabilities (mocked) assert request_null_capability.capability_info.lookup_capability("test_create") @@ -143,3 +143,73 @@ def test_edit_capability_error(test_config: Configurator, request_null_capabilit response_edit_failed = edit_capability(request_null_capability) assert response_edit_failed.status_code == 417 assert type(response_edit_failed) is HTTPExpectationFailed + + +def test_enable_capability(test_config: Configurator, request_null_capability: DummyRequest): + """ + Tests that capabilities can be properly enabled + + :param test_config: Dummy Pyramid Configurator object set up for testing + :param request_null_capability: Dummy Pyramid request object set up with mocked DB access + supporting the null capability + """ + from capability.views.capability import enable_capability + + request_null_capability.matchdict["capability_name"] = "null" + response = enable_capability(request_null_capability) + assert response.status_code == 200 + + +def test_enable_capability_error(test_config: Configurator, request_null_capability: DummyRequest): + """ + Tests that enable_capability view properly responds with HTTP exceptions given bad input + + :param test_config: Dummy Pyramid Configurator object set up for testing + :param request_null_capability: Dummy Pyramid request object set up with mocked DB access + supporting the null capability + """ + from capability.views.capability import enable_capability + + request_null_capability.matchdict["capability_name"] = "does_not_exist" + response_does_not_exist = enable_capability(request_null_capability) + assert response_does_not_exist.status_code == 412 + assert type(response_does_not_exist) is HTTPPreconditionFailed + request_null_capability.matchdict["capability_name"] = "error" + response_enable_failed = enable_capability(request_null_capability) + assert response_enable_failed.status_code == 417 + assert type(response_enable_failed) is HTTPExpectationFailed + + +def test_disable_capability(test_config: Configurator, request_null_capability: DummyRequest): + """ + Tests that capabilities can be properly disabled + + :param test_config: Dummy Pyramid Configurator object set up for testing + :param request_null_capability: Dummy Pyramid request object set up with mocked DB access + supporting the null capability + """ + from capability.views.capability import disable_capability + + request_null_capability.matchdict["capability_name"] = "null" + response = disable_capability(request_null_capability) + assert response.status_code == 200 + + +def test_disable_capability_error(test_config: Configurator, request_null_capability: DummyRequest): + """ + Tests that disable_capability view properly responds with HTTP exceptions given bad input + + :param test_config: Dummy Pyramid Configurator object set up for testing + :param request_null_capability: Dummy Pyramid request object set up with mocked DB access + supporting the null capability + """ + from capability.views.capability import disable_capability + + request_null_capability.matchdict["capability_name"] = "does_not_exist" + response_does_not_exist = disable_capability(request_null_capability) + assert response_does_not_exist.status_code == 412 + assert type(response_does_not_exist) is HTTPPreconditionFailed + request_null_capability.matchdict["capability_name"] = "error" + response_disable_failed = disable_capability(request_null_capability) + assert response_disable_failed.status_code == 417 + assert type(response_disable_failed) is HTTPExpectationFailed diff --git a/shared/workspaces/workspaces/capability/schema.py b/shared/workspaces/workspaces/capability/schema.py index e2b7e025a..0905ffb10 100644 --- a/shared/workspaces/workspaces/capability/schema.py +++ b/shared/workspaces/workspaces/capability/schema.py @@ -54,6 +54,7 @@ class Capability(Base, CapabilityIF): name = sa.Column("capability_name", sa.String, primary_key=True) steps = sa.Column("capability_steps", sa.String) max_jobs = sa.Column("max_jobs", sa.Integer) + enabled = sa.Column("enabled", sa.Boolean, default=True, server_default="true") requests = relationship("CapabilityRequest", back_populates="capability") @classmethod @@ -81,6 +82,8 @@ class Capability(Base, CapabilityIF): self.name = json_dict["name"] self.max_jobs = json_dict["max_jobs"] self.steps = json_dict["steps"] + self.enabled = json_dict.get("enabled", True) + return self @staticmethod @@ -118,11 +121,12 @@ class Capability(Base, CapabilityIF): "name": self.name, "max_jobs": self.max_jobs, "steps": str(self.steps), + "enabled": self.enabled, } def __str__(self): return ( - f"Capability object with name {self.name} and max simultaneous jobs of {self.max_jobs}" + f"Capability: {self.name}, max concurrent jobs of {self.max_jobs}" f"\nSequence: {self.steps}" ) diff --git a/shared/workspaces/workspaces/capability/services/capability_info.py b/shared/workspaces/workspaces/capability/services/capability_info.py index 944e9ae80..71cb7a5d3 100644 --- a/shared/workspaces/workspaces/capability/services/capability_info.py +++ b/shared/workspaces/workspaces/capability/services/capability_info.py @@ -49,7 +49,11 @@ class CapabilityInfo(CapabilityInfoIF): return capability def edit_capability( - self, name: CapabilityName, steps: CapabilitySequence = None, max_jobs: int = None + self, + name: CapabilityName, + steps: CapabilitySequence = None, + max_jobs: int = None, + enabled: bool = None, ) -> bool: """ Edit existing capability definition @@ -57,13 +61,16 @@ class CapabilityInfo(CapabilityInfoIF): :param name: Name of capability to edit :param steps: New capability sequence or None if wanting to leave unchanged :param max_jobs: New number of max jobs or None if wanting to leave unchanged + :param enabled: New enabled state or None is wanting to leave unchanges :return: True if the capability was successfully edited, else False """ changes = {} - if steps: + if steps is not None: changes[Capability.steps] = steps - if max_jobs: + if max_jobs is not None: changes[Capability.max_jobs] = max_jobs + if enabled is not None: + changes[Capability.enabled] = enabled if changes: rows_changed = self.session.query(Capability).filter_by(name=name).update(changes) -- GitLab