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