From 84f8cf515f94486db2b5d58d3a3ec2c2f754e01b Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 1 Nov 2023 11:22:18 -0400 Subject: [PATCH 01/14] Adding .python-version to .gitignore Signed-off-by: Cliff Hill --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index f60b72b58..76bfb9dc0 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,9 @@ var/ *.manifest *.spec +# Pyenv +.python-version + # Installer logs pip-log.txt pip-delete-this-directory.txt From d80e503dedc6fea866ab6c572cd0a8acc27833ec Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 15 Nov 2023 17:16:53 -0500 Subject: [PATCH 02/14] Getting Agreement model built. Signed-off-by: Cliff Hill --- app/models.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/models.py b/app/models.py index 4d8e97c3b..bfd0c9a21 100644 --- a/app/models.py +++ b/app/models.py @@ -360,6 +360,31 @@ class Domain(db.Model): ) +class AgreementType(Enum): + MOU = "MOU" + IAA = "IAA" + + +class Agreement(db.Model): + __tablename__ = "agreement" + + id = db.Column( + UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=False + ) + type = db.Column( + db.Enum(AgreementType, name="agreement_types"), + index=False, + unique=False, + nullable=False, + ) + partner_name = db.Column(db.String(255), primary_key=True) + status + start + end + url + budget_amount + + ORGANIZATION_TYPES = ["federal", "state", "other"] From 15b6cd38a49d8256f3e2687d85f5ad983a7c6526 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 15 Nov 2023 17:24:58 -0500 Subject: [PATCH 03/14] Almost all fields defined for Agreement. Signed-off-by: Cliff Hill --- app/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models.py b/app/models.py index bfd0c9a21..8b23b4c11 100644 --- a/app/models.py +++ b/app/models.py @@ -378,11 +378,11 @@ class Agreement(db.Model): nullable=False, ) partner_name = db.Column(db.String(255), primary_key=True) - status - start - end - url - budget_amount + # status + start = db.Column(db.DateTime, nullable=False) + end = db.Column(db.DateTime, nullable=False) + url = db.Column(db.String(2000), nullable=True) + budget_amount = db.Column(db.Float, nullable=True) ORGANIZATION_TYPES = ["federal", "state", "other"] From bd8de68d2ef5737886f4b7a0c6156091809ddb62 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 15 Nov 2023 17:28:56 -0500 Subject: [PATCH 04/14] Almost all fields for Agreements defined. Signed-off-by: Cliff Hill --- app/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models.py b/app/models.py index 8b23b4c11..db9b59ca8 100644 --- a/app/models.py +++ b/app/models.py @@ -378,11 +378,17 @@ class Agreement(db.Model): nullable=False, ) partner_name = db.Column(db.String(255), primary_key=True) - # status + # TODO: status start = db.Column(db.DateTime, nullable=False) end = db.Column(db.DateTime, nullable=False) url = db.Column(db.String(2000), nullable=True) budget_amount = db.Column(db.Float, nullable=True) + organization_id = db.Column( + UUID(as_uuid=True), + db.ForeignKey("organization.id"), + nullable=True, + ) + organization = db.relationship("Organization", backref="agreements") ORGANIZATION_TYPES = ["federal", "state", "other"] From 2baebee1998daf9ee2323f72c5151124ec50c719 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Thu, 16 Nov 2023 13:00:21 -0500 Subject: [PATCH 05/14] Got the agreement statuses added. Signed-off-by: Cliff Hill --- app/models.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/models.py b/app/models.py index db9b59ca8..1345028e6 100644 --- a/app/models.py +++ b/app/models.py @@ -1,6 +1,7 @@ import datetime import itertools import uuid +from enum import Enum from flask import current_app, url_for from notifications_utils.clients.encryption.encryption_client import EncryptionError @@ -365,8 +366,13 @@ class AgreementType(Enum): IAA = "IAA" +class AgreementStatus(Enum): + ACTIVE = "active" + EXPIRED = "expired" + + class Agreement(db.Model): - __tablename__ = "agreement" + __tablename__ = "agreements" id = db.Column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=False @@ -378,7 +384,12 @@ class Agreement(db.Model): nullable=False, ) partner_name = db.Column(db.String(255), primary_key=True) - # TODO: status + status = db.Column( + db.Enum(AgreementStatus, name="agreement_statuses"), + index=False, + unique=False, + nullable=False, + ) start = db.Column(db.DateTime, nullable=False) end = db.Column(db.DateTime, nullable=False) url = db.Column(db.String(2000), nullable=True) From ca8788cc6c48a7b73de1d750d88b6894bc9a4e14 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 17 Nov 2023 15:37:38 -0500 Subject: [PATCH 06/14] Tweaked where Agreement is at. Signed-off-by: Cliff Hill --- app/models.py | 66 ++++++++++++++++++--------------------------------- 1 file changed, 23 insertions(+), 43 deletions(-) diff --git a/app/models.py b/app/models.py index 1345028e6..46d3d69bf 100644 --- a/app/models.py +++ b/app/models.py @@ -361,47 +361,6 @@ class Domain(db.Model): ) -class AgreementType(Enum): - MOU = "MOU" - IAA = "IAA" - - -class AgreementStatus(Enum): - ACTIVE = "active" - EXPIRED = "expired" - - -class Agreement(db.Model): - __tablename__ = "agreements" - - id = db.Column( - UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=False - ) - type = db.Column( - db.Enum(AgreementType, name="agreement_types"), - index=False, - unique=False, - nullable=False, - ) - partner_name = db.Column(db.String(255), primary_key=True) - status = db.Column( - db.Enum(AgreementStatus, name="agreement_statuses"), - index=False, - unique=False, - nullable=False, - ) - start = db.Column(db.DateTime, nullable=False) - end = db.Column(db.DateTime, nullable=False) - url = db.Column(db.String(2000), nullable=True) - budget_amount = db.Column(db.Float, nullable=True) - organization_id = db.Column( - UUID(as_uuid=True), - db.ForeignKey("organization.id"), - nullable=True, - ) - organization = db.relationship("Organization", backref="agreements") - - ORGANIZATION_TYPES = ["federal", "state", "other"] @@ -2395,14 +2354,34 @@ class WebauthnCredential(db.Model): } +class AgreementType(Enum): + MOU = "MOU" + IAA = "IAA" + + +class AgreementStatus(Enum): + ACTIVE = "active" + EXPIRED = "expired" + + class Agreement(db.Model): __tablename__ = "agreements" id = db.Column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=False ) - type = db.Column(db.String(3), nullable=False, unique=True, index=True) + type = db.Column( + db.Enum(AgreementType, name="agreement_types"), + index=False, + unique=False, + nullable=False, + ) partner_name = db.Column(db.String(255), nullable=False, unique=True, index=True) - status = db.Column(db.String(255), nullable=False, unique=True, index=True) + status = db.Column( + db.Enum(AgreementStatus, name="agreement_statuses"), + index=False, + unique=False, + nullable=False, + ) start_time = db.Column(db.DateTime, nullable=True) end_time = db.Column(db.DateTime, nullable=True) url = db.Column(db.String(255), nullable=False, unique=True, index=True) @@ -2412,6 +2391,7 @@ class Agreement(db.Model): db.ForeignKey("organization.id"), nullable=True, ) + organization = db.relationship("Organization", backref="agreements") def serialize(self): return { From 901aaed157bd2552c41f4083ab25e8d786636d26 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 17 Nov 2023 16:14:56 -0500 Subject: [PATCH 07/14] Migration configured Signed-off-by: Cliff Hill --- .../versions/0405_adjust_agreement_model.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 migrations/versions/0405_adjust_agreement_model.py diff --git a/migrations/versions/0405_adjust_agreement_model.py b/migrations/versions/0405_adjust_agreement_model.py new file mode 100644 index 000000000..3811f834e --- /dev/null +++ b/migrations/versions/0405_adjust_agreement_model.py @@ -0,0 +1,52 @@ +""" + +Revision ID: eb7747053d5d +Revises: 0404_expire_invites +Create Date: 2023-11-17 15:39:45.470089 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +revision = '0405_adjust_agreement_model' +down_revision = '0404_expire_invites' + +agreement_type_name = 'agreement_types' +agreement_type_options = ("MOU", "IAA") +agreement_types = sa.Enum(*agreement_type_options, name=agreement_type_name) + +agreement_status_name = 'agreement_statuses' +agreement_status_options = ("active", "expired") +agreement_statuses = sa.Enum(*agreement_status_options, name=agreement_status_name) + +def upgrade(): + agreement_types.create(op.get_bind()) + op.execute( + f"ALTER TABLE agreements ALTER COLUMN type TYPE {agreement_type_name} using type::text::{agreement_type_name}" + ) + + agreement_statuses.create(op.get_bind()) + op.execute( + f"ALTER TABLE agreements ALTER COLUMN status TYPE {agreement_status_name} using status::text::{agreement_status_name}" + ) + + +def downgrade(): + op.alter_column( + 'agreements', + 'status', + existing_type=agreement_statuses, + type_=sa.VARCHAR(length=255), + existing_nullable=False, + ) + op.execute(f"DROP TYPE {agreement_status_name}") + + op.alter_column( + 'agreements', + 'type', + existing_type=agreement_types, + type_=sa.VARCHAR(length=3), + existing_nullable=False, + ) + op.execute(f"DROP TYPE {agreement_type_name}") From 33c16bdd1e4166cfeee9b2417c375c707d995d19 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Mon, 20 Nov 2023 09:43:40 -0500 Subject: [PATCH 08/14] Added is_active and has_mou to Organization model. Signed-off-by: Cliff Hill --- app/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models.py b/app/models.py index 46d3d69bf..89be0bb9c 100644 --- a/app/models.py +++ b/app/models.py @@ -434,6 +434,14 @@ class Organization(db.Model): def domain_list(self): return [domain.domain for domain in self.domains] + @property + def is_active(self): + return self.agreement and self.agreement.status == AgreementStatus.active + + @property + def has_mou(self): + return self.agreement and self.agreement.type == AgreementType.MOU + def serialize(self): return { "id": str(self.id), From d1dec155b68d662447fe8555936632cf979e5e30 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Mon, 20 Nov 2023 12:31:57 -0500 Subject: [PATCH 09/14] Improving tests a little. Signed-off-by: Cliff Hill --- app/models.py | 12 ++++++++++++ .../versions/0405_adjust_agreement_model.py | 17 +++++++++-------- tests/app/dao/test_invited_user_dao.py | 6 ++++-- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/models.py b/app/models.py index 89be0bb9c..5e7c46d9e 100644 --- a/app/models.py +++ b/app/models.py @@ -434,6 +434,18 @@ class Organization(db.Model): def domain_list(self): return [domain.domain for domain in self.domains] + @property + def agreement(self): + try: + active_agreements = [ + agreement + for agreement in self.agreements + if agreement.status == AgreementStatus.ACTIVE + ] + return active_agreements[0] + except IndexError: + return None + @property def is_active(self): return self.agreement and self.agreement.status == AgreementStatus.active diff --git a/migrations/versions/0405_adjust_agreement_model.py b/migrations/versions/0405_adjust_agreement_model.py index 3811f834e..002b9afb1 100644 --- a/migrations/versions/0405_adjust_agreement_model.py +++ b/migrations/versions/0405_adjust_agreement_model.py @@ -9,17 +9,18 @@ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql -revision = '0405_adjust_agreement_model' -down_revision = '0404_expire_invites' +revision = "0405_adjust_agreement_model" +down_revision = "0404_expire_invites" -agreement_type_name = 'agreement_types' +agreement_type_name = "agreement_types" agreement_type_options = ("MOU", "IAA") agreement_types = sa.Enum(*agreement_type_options, name=agreement_type_name) -agreement_status_name = 'agreement_statuses' +agreement_status_name = "agreement_statuses" agreement_status_options = ("active", "expired") agreement_statuses = sa.Enum(*agreement_status_options, name=agreement_status_name) + def upgrade(): agreement_types.create(op.get_bind()) op.execute( @@ -34,8 +35,8 @@ def upgrade(): def downgrade(): op.alter_column( - 'agreements', - 'status', + "agreements", + "status", existing_type=agreement_statuses, type_=sa.VARCHAR(length=255), existing_nullable=False, @@ -43,8 +44,8 @@ def downgrade(): op.execute(f"DROP TYPE {agreement_status_name}") op.alter_column( - 'agreements', - 'type', + "agreements", + "type", existing_type=agreement_types, type_=sa.VARCHAR(length=3), existing_nullable=False, diff --git a/tests/app/dao/test_invited_user_dao.py b/tests/app/dao/test_invited_user_dao.py index 4b49c1948..c1994cf56 100644 --- a/tests/app/dao/test_invited_user_dao.py +++ b/tests/app/dao/test_invited_user_dao.py @@ -133,16 +133,18 @@ def test_should_delete_all_invitations_more_than_one_day_old( def test_should_not_delete_invitations_less_than_two_days_old( sample_user, sample_service ): + two_days = timedelta(days=2) + one_second = timedelta(seconds=1) make_invitation( sample_user, sample_service, - age=timedelta(hours=47, minutes=59, seconds=59), + age=two_days - one_second, # Not quite two days email_address="valid@2.com", ) make_invitation( sample_user, sample_service, - age=timedelta(hours=48), + age=two_days, email_address="expired@1.com", ) From d433fbdeda086578fef81da7cf52bb1b25e76a90 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Tue, 21 Nov 2023 12:20:00 -0500 Subject: [PATCH 10/14] Making adjustments to models. Signed-off-by: Cliff Hill --- app/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models.py b/app/models.py index 5e7c46d9e..1a7247625 100644 --- a/app/models.py +++ b/app/models.py @@ -447,7 +447,7 @@ class Organization(db.Model): return None @property - def is_active(self): + def agreement_active(self): return self.agreement and self.agreement.status == AgreementStatus.active @property From c4800a746d4c35c2298e36a3833ae9bf7cd52067 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 22 Nov 2023 08:41:15 -0500 Subject: [PATCH 11/14] Adjusted migration. Signed-off-by: Cliff Hill --- ...just_agreement_model.py => 0406_adjust_agreement_model.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename migrations/versions/{0405_adjust_agreement_model.py => 0406_adjust_agreement_model.py} (94%) diff --git a/migrations/versions/0405_adjust_agreement_model.py b/migrations/versions/0406_adjust_agreement_model.py similarity index 94% rename from migrations/versions/0405_adjust_agreement_model.py rename to migrations/versions/0406_adjust_agreement_model.py index 002b9afb1..4b91b8c9a 100644 --- a/migrations/versions/0405_adjust_agreement_model.py +++ b/migrations/versions/0406_adjust_agreement_model.py @@ -9,8 +9,8 @@ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql -revision = "0405_adjust_agreement_model" -down_revision = "0404_expire_invites" +revision = "0406_adjust_agreement_model" +down_revision = "0405_add_preferred_timezone" agreement_type_name = "agreement_types" agreement_type_options = ("MOU", "IAA") From 68c50ff824dfe5f3f983efa5eceff862c71f32cb Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 22 Nov 2023 11:55:51 -0500 Subject: [PATCH 12/14] Tests being configured. Signed-off-by: Cliff Hill --- app/models.py | 6 ++-- tests/app/dao/test_invited_user_dao.py | 2 +- tests/app/test_model.py | 43 ++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/app/models.py b/app/models.py index 1a7247625..a90ab65dc 100644 --- a/app/models.py +++ b/app/models.py @@ -448,11 +448,13 @@ class Organization(db.Model): @property def agreement_active(self): - return self.agreement and self.agreement.status == AgreementStatus.active + return ( + self.agreement.status == AgreementStatus.active if self.agreement else False + ) @property def has_mou(self): - return self.agreement and self.agreement.type == AgreementType.MOU + return self.agreement.type == AgreementType.MOU if self.agreement else False def serialize(self): return { diff --git a/tests/app/dao/test_invited_user_dao.py b/tests/app/dao/test_invited_user_dao.py index c1994cf56..b0a7d75de 100644 --- a/tests/app/dao/test_invited_user_dao.py +++ b/tests/app/dao/test_invited_user_dao.py @@ -138,7 +138,7 @@ def test_should_not_delete_invitations_less_than_two_days_old( make_invitation( sample_user, sample_service, - age=two_days - one_second, # Not quite two days + age=two_days - one_second, # Not quite two days email_address="valid@2.com", ) make_invitation( diff --git a/tests/app/test_model.py b/tests/app/test_model.py index e2a2d1221..c314c656c 100644 --- a/tests/app/test_model.py +++ b/tests/app/test_model.py @@ -15,6 +15,8 @@ from app.models import ( NOTIFICATION_TECHNICAL_FAILURE, SMS_TYPE, Agreement, + AgreementStatus, + AgreementType, AnnualBilling, Notification, NotificationHistory, @@ -28,6 +30,7 @@ from app.models import ( from tests.app.db import ( create_inbound_number, create_notification, + create_organization, create_rate, create_reply_to_email, create_service, @@ -412,6 +415,46 @@ def test_rate_str(): assert rate.__str__() == "1.5 sms 2023-01-01 00:00:00" +@pytest.mark.parametrize( + ["agreement_type", "expected"], + ( + (AgreementType.IAA, False), + (AgreementType.MOU, True), + ), +) +def test_organization_agreement_mou(notify_db_session, agreement_type, expected): + now = datetime.utcnow() + agree = Agreement() + agree.id = "whatever" + agree.start_time = now + agree.end_time = now + agree.status = AgreementStatus.ACTIVE + agree.type = agreement_type + organization = create_organization(name="Something") + organization.agreements.append(agree) + assert organization.has_mou == expected + + +@pytest.mark.parametrize( + ["agreement_status", "expected"], + ( + (AgreementStatus.EXPIRED, False), + (AgreementStatus.ACTIVE, True), + ), +) +def test_organization_agreement_active(notify_db_session, agreement_status, expected): + now = datetime.utcnow() + agree = Agreement() + agree.id = "whatever" + agree.start_time = now + agree.end_time = now + agree.status = agreement_status + agree.type = AgreementType.IAA + organization = create_organization(name="Something") + organization.agreements.append(agree) + assert organization.active == expected + + def test_agreement_serialize(): agree = Agreement() agree.id = "abc" From fe01de3668672324ef11615d462c43ca1799e8c0 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 22 Nov 2023 12:16:24 -0500 Subject: [PATCH 13/14] More testing tweaks. Signed-off-by: Cliff Hill --- app/models.py | 12 ++++++++---- tests/app/test_model.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/models.py b/app/models.py index a90ab65dc..d141d8c29 100644 --- a/app/models.py +++ b/app/models.py @@ -448,13 +448,17 @@ class Organization(db.Model): @property def agreement_active(self): - return ( - self.agreement.status == AgreementStatus.active if self.agreement else False - ) + try: + return self.agreement.status == AgreementStatus.ACTIVE + except AttributeError: + return False @property def has_mou(self): - return self.agreement.type == AgreementType.MOU if self.agreement else False + try: + return self.agreement.type == AgreementType.MOU + except AttributeError: + return False def serialize(self): return { diff --git a/tests/app/test_model.py b/tests/app/test_model.py index c314c656c..faab07182 100644 --- a/tests/app/test_model.py +++ b/tests/app/test_model.py @@ -452,7 +452,7 @@ def test_organization_agreement_active(notify_db_session, agreement_status, expe agree.type = AgreementType.IAA organization = create_organization(name="Something") organization.agreements.append(agree) - assert organization.active == expected + assert organization.agreement_active == expected def test_agreement_serialize(): From 2ddffcfcba0993cbc98c753433094536e463c71b Mon Sep 17 00:00:00 2001 From: Carlo Costino Date: Wed, 22 Nov 2023 13:39:27 -0500 Subject: [PATCH 14/14] Update .gitignore Removing duplicate `.gitignore` entry for `.python-version` --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 76bfb9dc0..f60b72b58 100644 --- a/.gitignore +++ b/.gitignore @@ -36,9 +36,6 @@ var/ *.manifest *.spec -# Pyenv -.python-version - # Installer logs pip-log.txt pip-delete-this-directory.txt