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