From 7dc12f3d639a5d6bdc46f789ec5a4a21a5303012 Mon Sep 17 00:00:00 2001
From: Sam Kagan <skagan@nrao.edu>
Date: Wed, 11 Aug 2021 10:52:33 -0600
Subject: [PATCH 1/4] Added repo methods and API endpoint to list open
 Solicitations

---
 .../abstract_repository/solicitation.py       |  7 ++++-
 .../repository/orm_repository/orm_model.py    |  3 +-
 .../solicitation_orm_repository.py            |  3 ++
 .../solicitation_sql_repository.py            | 10 ++++++
 .../repository/test_solicitations_repo.py     | 14 +++++++++
 .../tests/solicitations/test_solicitations.py | 19 ++++++++++++
 .../ttat_rest_api/ttat_rest_api/routes.py     |  5 +++
 .../ttat_rest_api/views/solicitation.py       | 31 ++++++++++++++++++-
 8 files changed, 89 insertions(+), 3 deletions(-)

diff --git a/middle-layer/repository/abstract_repository/solicitation.py b/middle-layer/repository/abstract_repository/solicitation.py
index de190fdd3..4d99c6b86 100644
--- a/middle-layer/repository/abstract_repository/solicitation.py
+++ b/middle-layer/repository/abstract_repository/solicitation.py
@@ -17,7 +17,12 @@ from domainmodel.solicitation import (
 
 ProposalProcessRepository = SubRepository[ProposalProcess]
 NotificationGroupRepository = SubRepository[NotificationGroup]
-SolicitationRepository = SubRepository[Solicitation]
+
+
+class SolicitationRepository(SubRepository[Solicitation]):
+    @abc.abstractmethod
+    def list_open(self) -> List[Solicitation]:
+        raise NotImplementedError
 
 
 class CapabilityRepository(SubRepository[Capability]):
diff --git a/middle-layer/repository/orm_repository/orm_model.py b/middle-layer/repository/orm_repository/orm_model.py
index f2d281dc4..fbde4fd28 100644
--- a/middle-layer/repository/orm_repository/orm_model.py
+++ b/middle-layer/repository/orm_repository/orm_model.py
@@ -11,7 +11,7 @@ from sqlalchemy import (
     DateTime,
 )
 from sqlalchemy.engine import Engine
-from sqlalchemy.orm import registry, relationship, Session
+from sqlalchemy.orm import column_property, registry, relationship, Session
 
 from domainmodel.solicitation import (Facility, ProposalClass, ProposalProcess, NotificationGroup, ScienceCategory,
                                       Solicitation, SolicitationCapability, Capability, CapabilitySpec)
@@ -214,6 +214,7 @@ def map_orm(engine: Engine) -> None:
         Solicitation,
         solicitation,
         properties={
+            "_is_open": solicitation.c.is_open,
             "capabilities": relationship(Capability, backref="solicitation"),
             "proposal_process": relationship(ProposalProcess, backref="solicitations"),
             "notification_group": relationship(
diff --git a/middle-layer/repository/orm_repository/solicitation_orm_repository.py b/middle-layer/repository/orm_repository/solicitation_orm_repository.py
index e2b787fb2..d842c83c0 100644
--- a/middle-layer/repository/orm_repository/solicitation_orm_repository.py
+++ b/middle-layer/repository/orm_repository/solicitation_orm_repository.py
@@ -23,6 +23,9 @@ class SolicitationORMRepository(SolicitationRepository):
     def list(self) -> List[Solicitation]:
         return list(list_entities(self.session, Solicitation))
 
+    def list_open(self) -> List[Solicitation]:
+        return list(self.session.query(Solicitation).filter(Solicitation._is_open))
+
     def add(self, solicitation: Solicitation) -> int:
         try:
             add_entity(self.session, solicitation)
diff --git a/middle-layer/repository/sql_repository/solicitation_sql_repository.py b/middle-layer/repository/sql_repository/solicitation_sql_repository.py
index 5c6bf1726..0dbeccaf5 100644
--- a/middle-layer/repository/sql_repository/solicitation_sql_repository.py
+++ b/middle-layer/repository/sql_repository/solicitation_sql_repository.py
@@ -95,6 +95,16 @@ class SolicitationSQLRepository(SolicitationRepository, SQLEngineConsumer):
                 solicitation_ids.append(row.solicitation_id)
         return self.fetch(solicitation_ids)
 
+    def list_open(self) -> List[Solicitation]:
+        solicitation_ids: List[int] = []
+        with self.engine.connect() as conn:
+            result = conn.execute(
+                text(f"SELECT solicitation_id from solicitations WHERE is_open")
+            )
+            for row in result:
+                solicitation_ids.append(row.solicitation_id)
+        return self.fetch(solicitation_ids)
+
     def add(self, solicitation: Solicitation) -> int:
         with self.engine.connect() as conn:
             with conn.begin():
diff --git a/middle-layer/tests/repository/test_solicitations_repo.py b/middle-layer/tests/repository/test_solicitations_repo.py
index 34d555a84..2a848a526 100644
--- a/middle-layer/tests/repository/test_solicitations_repo.py
+++ b/middle-layer/tests/repository/test_solicitations_repo.py
@@ -42,6 +42,20 @@ def test_solicitation_list():
     assert len(solicitations) > 0
 
 
+def test_solicitation_list_open():
+    solicitations = repo.solicitation_repo.list_open()
+    assert len(solicitations) > 0
+    # Test (being in list_open <=> is_open is True) <=>
+    #       (being in list_open => is_open is True && is_open is True => being in list_open) <=>
+    #       (being in list_open => is_open is True && not being in list_open => is_open is False)
+    # Test (being in list_open() => is_open is True)
+    for solicitation in solicitations:
+        assert solicitation.is_open
+    # Test (not being in list_open() => is_open is False)
+    for solicitation in repo.solicitation_repo.list():
+        assert solicitation in solicitations or not solicitation.is_open
+
+
 def test_update_solicitation():
     global solicitation_id
     solicitation = repo.solicitation_repo.by_id(solicitation_id)
diff --git a/middle-layer/tests/solicitations/test_solicitations.py b/middle-layer/tests/solicitations/test_solicitations.py
index 028fad4a3..9ce85d93c 100644
--- a/middle-layer/tests/solicitations/test_solicitations.py
+++ b/middle-layer/tests/solicitations/test_solicitations.py
@@ -169,6 +169,25 @@ def test_list_solicitations():
     assert len(res.json()) > 0
 
 
+def test_list_open_solicitations():
+    url = f"{solicitation_endpoint}/list_open"
+    res = requests.get(url, headers={"Authorization": f"JWT {tta_member_token}"})
+    assert res.status_code == 200
+    res_json = res.json()
+    assert len(res_json) > 0
+    # Test (being in list_open <=> is_open is True) <=>
+    #       (being in list_open => is_open is True && is_open is True => being in list_open) <=>
+    #       (being in list_open => is_open is True && not being in list_open => is_open is False)
+    # Test (being in list_open => is_open is True)
+    for solicitation in res_json:
+        assert solicitation["is_open"]
+    # Test (not being in list_open => is_open is False)
+    for solicitation in requests.get(
+        solicitation_endpoint, headers={"Authorization": f"JWT {tta_member_token}"}
+    ).json():
+        assert solicitation in res_json or not solicitation["is_open"]
+
+
 def test_update_solicitation():
     global solicitation_id
     url = f'{solicitation_endpoint}/{solicitation_id}'
diff --git a/middle-layer/ttat_rest_api/ttat_rest_api/routes.py b/middle-layer/ttat_rest_api/ttat_rest_api/routes.py
index 4e62939e6..1e718e553 100644
--- a/middle-layer/ttat_rest_api/ttat_rest_api/routes.py
+++ b/middle-layer/ttat_rest_api/ttat_rest_api/routes.py
@@ -136,6 +136,11 @@ def solicitation_routes(config: Configurator):
     config.add_route(
         name="solicitations_list", pattern=solicitation_url, request_method="GET"
     )
+    config.add_route(
+        name="solicitations_list_open",
+        pattern=f"{solicitation_url}/list_open",
+        request_method="GET",
+    )
     config.add_route(
         name="solicitation_by_id", pattern=single_solicitation_url, request_method="GET"
     )
diff --git a/middle-layer/ttat_rest_api/ttat_rest_api/views/solicitation.py b/middle-layer/ttat_rest_api/ttat_rest_api/views/solicitation.py
index 9dd995d33..f5feec047 100644
--- a/middle-layer/ttat_rest_api/ttat_rest_api/views/solicitation.py
+++ b/middle-layer/ttat_rest_api/ttat_rest_api/views/solicitation.py
@@ -212,6 +212,7 @@ def add_solicitation_from_config_file(request: Request) -> Response:
     repo.solicitation_repo.add(new_solicitation)
     return Response(status=201, json_body=new_solicitation.__json__())
 
+
 @view_config(route_name="solicitations_list", renderer="json", permission="solicitations_list")
 def solicitations_list(request: Request) -> Response:
     """
@@ -233,6 +234,33 @@ def solicitations_list(request: Request) -> Response:
     return Response(json_body=response)
 
 
+@view_config(
+    route_name="solicitations_list_open",
+    renderer="json",
+    permission="solicitations_list",
+)
+def solicitations_list_open(request: Request) -> Response:
+    """
+    Pyramid view that accepts a request to list all open solicitations
+    URL: solicitations/list_open
+
+    :param request: GET request
+    :return: Response with JSON-formatted array of all open solicitations
+    """
+    solicitations = repo.solicitation_repo.list_open()
+    response = []
+
+    # Patch in mocked Solicitation for VLA/Continuum SolicitationCapability
+    vla_continuum_solicitation = get_vla_continuum_solicitation()
+    if vla_continuum_solicitation.is_open:
+        response.append(vla_continuum_solicitation.__json__())
+
+    for solicitation in solicitations:
+        response.append(solicitation.__json__())
+
+    return Response(json_body=response)
+
+
 @view_config(route_name="solicitation_by_id", renderer="json", permission="solicitation_by_id")
 def solicitation_by_id(request: Request) -> Response:
     """
@@ -386,9 +414,10 @@ def solicitation_delete(request: Request) -> Response:
     except NameError as e:
         return HTTPNotFound(detail=str(e))
 
+    json = solicitation.__json__()
     try:
         repo.solicitation_repo.delete(solicitation)
-        return Response(status=201, json_body=solicitation.__json__())
+        return Response(status=201, json_body=json)
     except ValueError as e:
         return HTTPExpectationFailed(detail=e)
 
-- 
GitLab


From 61dccade65811ffb50139047eee0de8a787d0fd9 Mon Sep 17 00:00:00 2001
From: Sam Kagan <skagan@nrao.edu>
Date: Wed, 11 Aug 2021 16:01:03 -0600
Subject: [PATCH 2/4] Set local timezone in ml/Dockerfile.* so that
 pendulum.now() will work

---
 Dockerfile.base               | 2 +-
 middle-layer/Dockerfile.dev   | 7 ++++++-
 middle-layer/Dockerfile.local | 5 +++++
 3 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/Dockerfile.base b/Dockerfile.base
index 84a5322a5..1c1a48d03 100644
--- a/Dockerfile.base
+++ b/Dockerfile.base
@@ -1,7 +1,7 @@
 FROM python:3.9-alpine
 
 # Install psycopg2 deps so schema and middle-layer can connect to Postgres
-RUN apk add \
+RUN apk --no-cache add \
     gcc \
     g++ \
     libc-dev \
diff --git a/middle-layer/Dockerfile.dev b/middle-layer/Dockerfile.dev
index d6ad5801d..9b31511d6 100644
--- a/middle-layer/Dockerfile.dev
+++ b/middle-layer/Dockerfile.dev
@@ -1,10 +1,15 @@
 # Middle/API layer Dockerfile for dev deployments, typically done via the GitLab CI pipeline
 FROM ssa-containers.aoc.nrao.edu/ops/base:ttat
 
+# Set local timezone so that pendulum.now() works
+RUN apk add alpine-conf \
+&& setup-timezone -z UTC \
+&& apk del alpine-conf
+
 WORKDIR /middle-layer
 COPY ./ ./
 RUN pip install -e /middle-layer/ttat_rest_api
-RUN pip install -e /middle-layer | tee /middlelayer_install.log
+RUN pip install -e /middle-layer
 
 WORKDIR /middle-layer/ttat_rest_api
 CMD ["pserve", "development.ini"]
diff --git a/middle-layer/Dockerfile.local b/middle-layer/Dockerfile.local
index 0c31b7c5e..41267d0bf 100644
--- a/middle-layer/Dockerfile.local
+++ b/middle-layer/Dockerfile.local
@@ -3,6 +3,11 @@
 #       New devlopers to the project or to Dockerfiles should first look at ./Dockerfile.dev
 FROM ssa-containers.aoc.nrao.edu/ops/base:ttat
 
+# Set local timezone so that pendulum.now() works
+RUN apk add alpine-conf \
+&& setup-timezone -z UTC \
+&& apk del alpine-conf
+
 # Install deps first since they take a while and the relevant files change infrequently
 # For main middle-layer package
 WORKDIR /middle-layer
-- 
GitLab


From 597aa0bd97760682e901f082c4381ac0d3543747 Mon Sep 17 00:00:00 2001
From: Sam Kagan <skagan@nrao.edu>
Date: Wed, 11 Aug 2021 16:01:16 -0600
Subject: [PATCH 3/4] Added static Solicitation.compute_is_open method to DM to
 tie Solicitation.is_open to call period

---
 middle-layer/domainmodel/solicitation.py         | 16 ++++++++++++++--
 .../tests/domainmodel/test_solicitation.py       | 14 ++++++++++++++
 .../tests/solicitations/test_solicitations.py    | 10 +++++++++-
 .../ttat_rest_api/views/solicitation.py          |  8 ++++++--
 .../2784182a7dfe_solicitation_schema_2.py        |  2 +-
 5 files changed, 44 insertions(+), 6 deletions(-)
 create mode 100644 middle-layer/tests/domainmodel/test_solicitation.py

diff --git a/middle-layer/domainmodel/solicitation.py b/middle-layer/domainmodel/solicitation.py
index a4d3bc414..2e592f383 100644
--- a/middle-layer/domainmodel/solicitation.py
+++ b/middle-layer/domainmodel/solicitation.py
@@ -278,9 +278,9 @@ class Solicitation:
         facilities: List[Facility] = [],
     ):
         self.solicitation_name = solicitation_name
-        self.is_open = is_open
         self.call_period_begin = pendulum.instance(call_period_begin)
         self.call_period_end = pendulum.instance(call_period_end)
+        self.is_open = Solicitation.compute_is_open(call_period_begin, call_period_end)
         self.proposal_process = proposal_process
         self.notification_group = notification_group
         # Should have only elements `c` such that c.facility == self.facility
@@ -294,7 +294,7 @@ class Solicitation:
             'solicitation_id': self.solicitation_id,
             'solicitation_name': str(self.solicitation_name),
             'is_open': bool(self.is_open),
-            # DateTimes and Interval serialized per https://www.postgresql.org/docs/current/datatype-datetime.html
+            # DateTimes serialized per https://www.postgresql.org/docs/current/datatype-datetime.html
             "call_period_begin": self.call_period_begin.isoformat(),
             "call_period_end": self.call_period_end.isoformat(),
             "proposal_process": self.proposal_process.__json__(),
@@ -305,3 +305,15 @@ class Solicitation:
             "facilities": [f.__json__() for f in self.facilities],
         }
 
+    @classmethod
+    def compute_is_open(
+        cls, call_period_begin: datetime, call_period_end: datetime
+    ) -> bool:
+        """Compute is_open based on call period
+
+        :param call_period_begin: Beginning of call period
+        :param call_period_end: End of call period
+        :return: True if and only if a Solicitation with these call-period bounds would be considered open
+        """
+        now = pendulum.now()
+        return now >= call_period_begin and now <= call_period_end
diff --git a/middle-layer/tests/domainmodel/test_solicitation.py b/middle-layer/tests/domainmodel/test_solicitation.py
new file mode 100644
index 000000000..be33d46a6
--- /dev/null
+++ b/middle-layer/tests/domainmodel/test_solicitation.py
@@ -0,0 +1,14 @@
+from domainmodel.solicitation import Solicitation
+import pendulum
+
+
+def test_compute_is_open_good_true():
+    call_period_begin = pendulum.parse("2021-08-10T00:00:00-00:00")
+    call_period_end = pendulum.parse("2025-08-10T00:00:00-00:00")
+    assert Solicitation.compute_is_open(call_period_begin, call_period_end)
+
+
+def test_compute_is_open_good_false():
+    call_period_begin = pendulum.parse("2011-08-10T00:00:00-00:00")
+    call_period_end = pendulum.parse("2015-08-10T00:00:00-00:00")
+    assert not Solicitation.compute_is_open(call_period_begin, call_period_end)
diff --git a/middle-layer/tests/solicitations/test_solicitations.py b/middle-layer/tests/solicitations/test_solicitations.py
index 9ce85d93c..d535e1358 100644
--- a/middle-layer/tests/solicitations/test_solicitations.py
+++ b/middle-layer/tests/solicitations/test_solicitations.py
@@ -3,6 +3,7 @@ import pendulum
 import json
 from typing import Any, Dict
 
+from domainmodel.solicitation import Solicitation
 from tests.rest_helpers import tta_member_token, base_url, randomword
 
 mock_solicitation_id = 0
@@ -20,7 +21,10 @@ def assert_solicitation_is_correct(
     actual: Dict[str, Any], expected: Dict[str, Any]
 ) -> None:
     assert actual["solicitation_name"] == expected["name"]
-    assert actual["is_open"] == expected["is_open"]
+    assert actual["is_open"] == Solicitation.compute_is_open(
+        pendulum.parse(expected["call_period_begin"]),
+        pendulum.parse(expected["call_period_end"]),
+    )
     assert (
         int(actual["proposal_process"]["proposal_process_id"])
         == expected["proposal_process"]["proposal_process_id"]
@@ -249,6 +253,10 @@ def test_update_solicitation_call_period_good():
         json["call_period_begin"], res.json()["call_period_begin"]
     )
     assert_datetime_is_correct(json["call_period_end"], res.json()["call_period_end"])
+    assert res.json()["is_open"] == Solicitation.compute_is_open(
+        pendulum.parse(json["call_period_begin"]),
+        pendulum.parse(json["call_period_end"]),
+    )
 
 
 def test_update_solicitation_call_period_good_mock_solicitation():
diff --git a/middle-layer/ttat_rest_api/ttat_rest_api/views/solicitation.py b/middle-layer/ttat_rest_api/ttat_rest_api/views/solicitation.py
index f5feec047..2ad581025 100644
--- a/middle-layer/ttat_rest_api/ttat_rest_api/views/solicitation.py
+++ b/middle-layer/ttat_rest_api/ttat_rest_api/views/solicitation.py
@@ -110,7 +110,8 @@ def construct_solicitation_from_json(
     except ParserError as e:
         raise ValueError(e)
     name = json["name"]
-    is_open = bool(json["is_open"])
+    # is_open = bool(json["is_open"])
+    is_open = Solicitation.compute_is_open(call_period_begin, call_period_end)
 
     # now for the solicitation itself
     solicitation = None
@@ -125,7 +126,7 @@ def construct_solicitation_from_json(
     else:
         solicitation = Solicitation(
             json["name"],
-            bool(json["is_open"]),
+            is_open,
             call_period_begin,
             call_period_end,
             proposal_process,
@@ -386,6 +387,9 @@ def solicitation_update_call_period(request: Request) -> Response:
         solicitation.call_period_end = parse_iso_8601_strings(params["call_period_end"])
     except ValueError or ParserError as e:
         return HTTPPreconditionFailed(detail=e)
+    solicitation.is_open = Solicitation.compute_is_open(
+        solicitation.call_period_begin, solicitation.call_period_end
+    )
 
     try:
         # Don't touch repo for Solicitation that's mocked for VLA/Continuum SolicitationCapability
diff --git a/schema/versions/2784182a7dfe_solicitation_schema_2.py b/schema/versions/2784182a7dfe_solicitation_schema_2.py
index 9f29d424f..3046d1f24 100644
--- a/schema/versions/2784182a7dfe_solicitation_schema_2.py
+++ b/schema/versions/2784182a7dfe_solicitation_schema_2.py
@@ -52,7 +52,7 @@ def upgrade():
     # Update test data to include new columns
     op.execute(
         f"UPDATE solicitations SET call_period_begin = '{datetime.datetime(2017, 2, 3)}', "
-        f"call_period_end = '{datetime.datetime(2017, 3, 4)}' WHERE solicitation_id = 1"
+        f"call_period_end = '{datetime.datetime(2027, 3, 4)}' WHERE solicitation_id = 1"
     )
 
     op.create_table(
-- 
GitLab


From 0c8a436c6c10bd345fd0ecbc6d498e4592e5dab1 Mon Sep 17 00:00:00 2001
From: Sam Kagan <skagan@nrao.edu>
Date: Wed, 11 Aug 2021 15:00:31 -0600
Subject: [PATCH 4/4] Made Solicitation.is_open private (i.e.
 Solicitation._is_open), created Sol.update_is_open to change it, removed it
 from JSON requested by API endpoints

---
 middle-layer/domainmodel/solicitation.py      | 23 ++++++++---
 .../solicitation_sql_repository.py            | 41 ++++++++++++++-----
 .../repository/vla_continuum_mock_repo.py     |  8 +++-
 .../repository/test_solicitations_repo.py     | 17 +++++---
 .../tests/solicitations/test_solicitations.py |  8 +---
 .../ttat_rest_api/views/solicitation.py       | 36 +++++++++-------
 6 files changed, 88 insertions(+), 45 deletions(-)

diff --git a/middle-layer/domainmodel/solicitation.py b/middle-layer/domainmodel/solicitation.py
index 2e592f383..7263fe464 100644
--- a/middle-layer/domainmodel/solicitation.py
+++ b/middle-layer/domainmodel/solicitation.py
@@ -253,7 +253,7 @@ class Solicitation:
     solicitation_id: int
     solicitation_name: str
     # Flag for if proposals can be accepted for this solicitation
-    is_open: bool
+    _is_open: bool
     call_period_begin: datetime
     call_period_end: datetime
     proposal_process: ProposalProcess
@@ -267,7 +267,6 @@ class Solicitation:
     def __init__(
         self,
         solicitation_name: str,
-        is_open: bool,
         call_period_begin: datetime,
         call_period_end: datetime,
         proposal_process: ProposalProcess,
@@ -280,7 +279,7 @@ class Solicitation:
         self.solicitation_name = solicitation_name
         self.call_period_begin = pendulum.instance(call_period_begin)
         self.call_period_end = pendulum.instance(call_period_end)
-        self.is_open = Solicitation.compute_is_open(call_period_begin, call_period_end)
+        self._is_open = Solicitation.compute_is_open(call_period_begin, call_period_end)
         self.proposal_process = proposal_process
         self.notification_group = notification_group
         # Should have only elements `c` such that c.facility == self.facility
@@ -291,9 +290,9 @@ class Solicitation:
 
     def __json__(self):
         return {
-            'solicitation_id': self.solicitation_id,
-            'solicitation_name': str(self.solicitation_name),
-            'is_open': bool(self.is_open),
+            "solicitation_id": self.solicitation_id,
+            "solicitation_name": str(self.solicitation_name),
+            "is_open": bool(self._is_open),
             # DateTimes serialized per https://www.postgresql.org/docs/current/datatype-datetime.html
             "call_period_begin": self.call_period_begin.isoformat(),
             "call_period_end": self.call_period_end.isoformat(),
@@ -317,3 +316,15 @@ class Solicitation:
         """
         now = pendulum.now()
         return now >= call_period_begin and now <= call_period_end
+
+    def update_is_open(self) -> bool:
+        """Update whether this Solicitation is open or not via self._is_open field,
+                based on self.call_period_(begin|end)
+        NB: This is the canonical way to change _is_open
+
+        :return: The new value of self._is_open
+        """
+        self._is_open = Solicitation.compute_is_open(
+            self.call_period_begin, self.call_period_end
+        )
+        return self._is_open
diff --git a/middle-layer/repository/sql_repository/solicitation_sql_repository.py b/middle-layer/repository/sql_repository/solicitation_sql_repository.py
index 0dbeccaf5..ed6e2b05a 100644
--- a/middle-layer/repository/sql_repository/solicitation_sql_repository.py
+++ b/middle-layer/repository/sql_repository/solicitation_sql_repository.py
@@ -61,15 +61,34 @@ class SolicitationSQLRepository(SolicitationRepository, SQLEngineConsumer):
                     facility_list = self.facility_repo.list_by_solicitation_id(row.solicitation_id)
                     call_begin = row.call_period_begin
                     call_end = row.call_period_end
-                    solicitation = Solicitation(row.solicitation_name, row.is_open,
-                                                pendulum.datetime(call_begin.year, call_begin.month, call_begin.day,
-                                                                  call_begin.hour, call_begin.minute, call_begin.second,
-                                                                  call_begin.microsecond, call_begin.tzinfo),
-                                                pendulum.datetime(call_end.year, call_end.month, call_end.day,
-                                                                  call_end.hour, call_end.minute, call_end.second,
-                                                                  call_end.microsecond, call_end.tzinfo),
-                                                proposal_process, notification_group, proposal_class_list,
-                                                science_category_list, facilities=facility_list)
+                    solicitation = Solicitation(
+                        row.solicitation_name,
+                        pendulum.datetime(
+                            call_begin.year,
+                            call_begin.month,
+                            call_begin.day,
+                            call_begin.hour,
+                            call_begin.minute,
+                            call_begin.second,
+                            call_begin.microsecond,
+                            call_begin.tzinfo,
+                        ),
+                        pendulum.datetime(
+                            call_end.year,
+                            call_end.month,
+                            call_end.day,
+                            call_end.hour,
+                            call_end.minute,
+                            call_end.second,
+                            call_end.microsecond,
+                            call_end.tzinfo,
+                        ),
+                        proposal_process,
+                        notification_group,
+                        proposal_class_list,
+                        science_category_list,
+                        facilities=facility_list,
+                    )
 
                     solicitation.solicitation_id = row.solicitation_id
                     # Should be list of SolicitationCapabilities
@@ -115,7 +134,7 @@ class SolicitationSQLRepository(SolicitationRepository, SQLEngineConsumer):
                             f"call_period_begin, "
                             f"call_period_end, "
                             f"proposal_process_id, notification_group_id) "
-                            f"VALUES ('{solicitation.solicitation_name}', {solicitation.is_open}, "
+                            f"VALUES ('{solicitation.solicitation_name}', {solicitation._is_open}, "
                             f"'{solicitation.call_period_begin.isoformat()}', "
                             f"'{solicitation.call_period_end.isoformat()}', "
                             f"'{solicitation.proposal_process.proposal_process_id}', "
@@ -149,7 +168,7 @@ class SolicitationSQLRepository(SolicitationRepository, SQLEngineConsumer):
                     text(
                         f"UPDATE solicitations SET "
                         f"solicitation_name = '{solicitation.solicitation_name}', "
-                        f"is_open = {solicitation.is_open}, "
+                        f"is_open = {solicitation._is_open}, "
                         f"call_period_begin = '{solicitation.call_period_begin.isoformat()}', "
                         f"call_period_end = '{solicitation.call_period_end.isoformat()}', "
                         f"proposal_process_id = {solicitation.proposal_process.proposal_process_id}, "
diff --git a/middle-layer/repository/vla_continuum_mock_repo.py b/middle-layer/repository/vla_continuum_mock_repo.py
index 9b7efd2fc..849be4cf7 100644
--- a/middle-layer/repository/vla_continuum_mock_repo.py
+++ b/middle-layer/repository/vla_continuum_mock_repo.py
@@ -49,7 +49,13 @@ def get_vla_continuum_solicitation() -> Solicitation:
     proposal_process.proposal_process_id = 0
     notification_group = NotificationGroup("a group for notification")
     notification_group.notification_group_id = 0
-    solicitation = Solicitation("SEM-22A", True, pendulum.datetime(2022, 3, 10), pendulum.datetime(2022, 10, 5), proposal_process, notification_group)
+    solicitation = Solicitation(
+        "SEM-22A",
+        pendulum.datetime(2022, 3, 10),
+        pendulum.datetime(2028, 10, 5),
+        proposal_process,
+        notification_group,
+    )
     solicitation.solicitation_id = 0
     return solicitation
 
diff --git a/middle-layer/tests/repository/test_solicitations_repo.py b/middle-layer/tests/repository/test_solicitations_repo.py
index 2a848a526..c81a5748b 100644
--- a/middle-layer/tests/repository/test_solicitations_repo.py
+++ b/middle-layer/tests/repository/test_solicitations_repo.py
@@ -23,9 +23,16 @@ def test_solicitation_add():
     assert len(proposal_classes) > 0
     science_categories = repo.science_category_repo.list()
     assert len(science_categories) > 0
-    solicitation = sm.Solicitation("repo test solicitation", True, pendulum.datetime(2010, 10, 3),
-                                   pendulum.datetime(2020, 5, 20), proposal_processes[0], notification_groups[0],
-                                   proposal_classes, science_categories, facilities=facilities)
+    solicitation = sm.Solicitation(
+        "repo test solicitation",
+        pendulum.datetime(2010, 10, 3),
+        pendulum.datetime(2020, 5, 20),
+        proposal_processes[0],
+        notification_groups[0],
+        proposal_classes,
+        science_categories,
+        facilities=facilities,
+    )
     solicitation_id = repo.solicitation_repo.add(solicitation)
     assert solicitation_id > 0
 
@@ -50,10 +57,10 @@ def test_solicitation_list_open():
     #       (being in list_open => is_open is True && not being in list_open => is_open is False)
     # Test (being in list_open() => is_open is True)
     for solicitation in solicitations:
-        assert solicitation.is_open
+        assert solicitation._is_open
     # Test (not being in list_open() => is_open is False)
     for solicitation in repo.solicitation_repo.list():
-        assert solicitation in solicitations or not solicitation.is_open
+        assert solicitation in solicitations or not solicitation._is_open
 
 
 def test_update_solicitation():
diff --git a/middle-layer/tests/solicitations/test_solicitations.py b/middle-layer/tests/solicitations/test_solicitations.py
index d535e1358..4a5a31e86 100644
--- a/middle-layer/tests/solicitations/test_solicitations.py
+++ b/middle-layer/tests/solicitations/test_solicitations.py
@@ -63,11 +63,10 @@ def test_add_solicitations_only_required_params():
     # this can fail if there is no proposal_process or notification_group with an id of 1
     json = {
         "name": randomword(10),
-        "is_open": True,
         "proposal_process": {"proposal_process_id": 1},
         "notification_group": {"notification_group_id": 1},
         "call_period_begin": pendulum.datetime(2020, 10, 10).to_iso8601_string(),
-        "call_period_end": pendulum.datetime(2020, 12, 12).to_iso8601_string(),
+        "call_period_end": pendulum.datetime(2030, 12, 12).to_iso8601_string(),
     }
     res = requests.post(
         solicitation_endpoint,
@@ -85,7 +84,6 @@ def test_add_solicitations_all_params():
     # this can fail if there is no proposal_process or notification_group with an id of 1
     json = {
         "name": randomword(10),
-        "is_open": True,
         "proposal_process": {"proposal_process_id": 1},
         "notification_group": {"notification_group_id": 1},
         "call_period_begin": pendulum.datetime(2020, 10, 10).to_iso8601_string(),
@@ -114,7 +112,6 @@ def test_add_solicitation_fail_existing_name():
     global solicitation_name
     json = {
         "name": solicitation_name,
-        "is_open": True,
         "proposal_process": {"proposal_process_id": 1},
         "notification_group": {"notification_group_id": 1},
         "call_period_begin": pendulum.datetime(2020, 10, 10).to_iso8601_string(),
@@ -132,7 +129,6 @@ def test_add_solicitation_fail_invalid_prerequisite():
     # TODO: Add unique constraint to pass this test
     json = {
         "name": randomword(10),
-        "is_open": True,
         "proposal_process": {"proposal_process_id": 99999},
         "notification_group": {"notification_group_id": 99999},
         "call_period_begin": pendulum.datetime(2020, 10, 10).to_iso8601_string(),
@@ -198,7 +194,6 @@ def test_update_solicitation():
     name = randomword(10)
     json = {
         "name": name,
-        "is_open": True,
         "proposal_process": {"proposal_process_id": 1},
         "notification_group": {"notification_group_id": 1},
         "call_period_begin": pendulum.datetime(2000, 10, 10).to_iso8601_string(),
@@ -217,7 +212,6 @@ def test_update_solicitation_mock_solicitation():
     name = randomword(10)
     json = {
         "name": name,
-        "is_open": True,
         "proposal_process": {"proposal_process_id": 1},
         "notification_group": {"notification_group_id": 1},
         "call_period_begin": pendulum.datetime(2000, 10, 10).to_iso8601_string(),
diff --git a/middle-layer/ttat_rest_api/ttat_rest_api/views/solicitation.py b/middle-layer/ttat_rest_api/ttat_rest_api/views/solicitation.py
index 2ad581025..d22a78a42 100644
--- a/middle-layer/ttat_rest_api/ttat_rest_api/views/solicitation.py
+++ b/middle-layer/ttat_rest_api/ttat_rest_api/views/solicitation.py
@@ -72,7 +72,6 @@ def construct_solicitation_from_json(
     :param json: A JSON object that looks like:
             {
                 "name": <string>,
-                "is_open": <boolean>,
                 "call_period_begin": <ISO 8601 datetime string>,
                 "call_period_end": <ISO 8601 datetime string>,
                 "proposal_process": {"proposal_process_id": <int>},
@@ -87,8 +86,13 @@ def construct_solicitation_from_json(
     :raises ValueError: If a child object is parsed incorrectly
     :raises KeyError: If json lacks one of the expected keys, shown above
     """
-    expected_keys = ["name", "is_open", "proposal_process", "notification_group", "call_period_begin",
-                     "call_period_end"]
+    expected_keys = [
+        "name",
+        "proposal_process",
+        "notification_group",
+        "call_period_begin",
+        "call_period_end",
+    ]
     if not all([expected in json.keys() for expected in expected_keys]):
         # JSON object does not contain all expected keys
         raise KeyError(f"Expected JSON object with all of keys in {expected_keys}. Received only {[key for key in json.keys()]}.")
@@ -110,23 +114,20 @@ def construct_solicitation_from_json(
     except ParserError as e:
         raise ValueError(e)
     name = json["name"]
-    # is_open = bool(json["is_open"])
-    is_open = Solicitation.compute_is_open(call_period_begin, call_period_end)
 
     # now for the solicitation itself
     solicitation = None
     if solicitation_to_update:
         solicitation = solicitation_to_update
         solicitation.solicitation_name = name
-        solicitation.is_open = is_open
         solicitation.call_period_begin = call_period_begin
         solicitation.call_period_end = call_period_end
         solicitation.proposal_process = proposal_process
         solicitation.notification_group = notification_group
+        solicitation.update_is_open()
     else:
         solicitation = Solicitation(
             json["name"],
-            is_open,
             call_period_begin,
             call_period_end,
             proposal_process,
@@ -208,8 +209,16 @@ def add_solicitation_from_config_file(request: Request) -> Response:
             facilities[i] = existing_facilities[existing_facility_names.index(facilities[i])]
 
     # using arbitrary notification group
-    new_solicitation = Solicitation(config['name'], False, call_period_begin, call_period_end, proposal_process,
-                                    repo.notification_group_repo.by_id(1), [], [], facilities=facilities)
+    new_solicitation = Solicitation(
+        config["name"],
+        call_period_begin,
+        call_period_end,
+        proposal_process,
+        repo.notification_group_repo.by_id(1),
+        [],
+        [],
+        facilities=facilities,
+    )
     repo.solicitation_repo.add(new_solicitation)
     return Response(status=201, json_body=new_solicitation.__json__())
 
@@ -253,7 +262,7 @@ def solicitations_list_open(request: Request) -> Response:
 
     # Patch in mocked Solicitation for VLA/Continuum SolicitationCapability
     vla_continuum_solicitation = get_vla_continuum_solicitation()
-    if vla_continuum_solicitation.is_open:
+    if vla_continuum_solicitation._is_open:
         response.append(vla_continuum_solicitation.__json__())
 
     for solicitation in solicitations:
@@ -293,7 +302,7 @@ def solicitation_add(request: Request) -> Response:
         or 400 response (HTTPBadRequest) if expected parameters not given
         or 412 response (HTTPPreconditionFailed) if solicitation with given name already exists or JSON parsing errors occur
     """
-    # to test: curl localhost:6543/solicitations -X POST -d '{ "name": "solicitation", "is_open": true,
+    # to test: curl localhost:6543/solicitations -X POST -d '{ "name": "solicitation",
     # "proposal_process": {"proposal_process_id": 1}, "notification_group": {"notification_group_id": 1},
     # "call_period_begin": "2020-12-12T10:00:01-00:05", "call_period_end": "2021-3-4T00:00:00-00:04"}'
     try:
@@ -387,10 +396,7 @@ def solicitation_update_call_period(request: Request) -> Response:
         solicitation.call_period_end = parse_iso_8601_strings(params["call_period_end"])
     except ValueError or ParserError as e:
         return HTTPPreconditionFailed(detail=e)
-    solicitation.is_open = Solicitation.compute_is_open(
-        solicitation.call_period_begin, solicitation.call_period_end
-    )
-
+    solicitation.update_is_open()
     try:
         # Don't touch repo for Solicitation that's mocked for VLA/Continuum SolicitationCapability
         if solicitation.solicitation_id != 0:
-- 
GitLab