diff --git a/app/enums.py b/app/enums.py index 2b5f2d603..4336375a5 100644 --- a/app/enums.py +++ b/app/enums.py @@ -106,9 +106,3 @@ class VerificationStatus(StrEnum): class AuthType(StrEnum): EMAIL_AUTH = "email_auth" SMS_AUTH = "sms_auth" - - -class ServiceStatus(StrEnum): - LIVE = "live" - TRIAL = "trial" - SUSPENDED = "suspended" diff --git a/app/main/views/organizations.py b/app/main/views/organizations.py index dc41474d0..84532d6b3 100644 --- a/app/main/views/organizations.py +++ b/app/main/views/organizations.py @@ -1,17 +1,11 @@ -from collections import Counter, OrderedDict +from collections import OrderedDict from datetime import datetime from functools import partial from flask import current_app, flash, redirect, render_template, request, url_for from flask_login import current_user -from app import ( - current_organization, - org_invite_api_client, - organizations_client, - service_api_client, -) -from app.enums import ServiceStatus +from app import current_organization, org_invite_api_client, organizations_client from app.main import main from app.main.forms import ( AdminBillingDetailsForm, @@ -68,28 +62,9 @@ def add_organization(): return render_template("views/organizations/add-organization.html", form=form) -def get_service_counts_by_status(services): - def get_status(service): - if not service.get("active"): - return ServiceStatus.SUSPENDED - elif service.get("restricted"): - return ServiceStatus.TRIAL - else: - return ServiceStatus.LIVE - - status_counts = Counter(get_status(service) for service in services) - - return { - "live_services": status_counts[ServiceStatus.LIVE], - "trial_services": status_counts[ServiceStatus.TRIAL], - "suspended_services": status_counts[ServiceStatus.SUSPENDED], - "total_services": len(services), - } - - def get_organization_message_allowance(org_id): try: - message_usage = service_api_client.get_organization_message_usage(org_id) + message_usage = organizations_client.get_organization_message_usage(org_id) except Exception as e: current_app.logger.error(f"Error fetching organization message usage: {e}") message_usage = {} @@ -110,14 +85,16 @@ def organization_dashboard(org_id): year = requested_and_current_financial_year(request)[0] message_allowance = get_organization_message_allowance(org_id) - service_counts = get_service_counts_by_status(current_organization.services) return render_template( "views/organizations/organization/index.html", selected_year=year, services=current_organization.services, + live_services=len(current_organization.live_services), + trial_services=len(current_organization.trial_services), + suspended_services=len(current_organization.suspended_services), + total_services=len(current_organization.services), **message_allowance, - **service_counts, ) diff --git a/app/models/organization.py b/app/models/organization.py index f2dc1bf07..33f5f3ec4 100644 --- a/app/models/organization.py +++ b/app/models/organization.py @@ -103,7 +103,11 @@ class Organization(JSONModel, SortByNameMixin): @property def trial_services(self): - return [s for s in self.services if not s["active"] or s["restricted"]] + return [s for s in self.services if s["active"] and s["restricted"]] + + @property + def suspended_services(self): + return [s for s in self.services if not s["active"]] @cached_property def invited_users(self): diff --git a/app/notify_client/organizations_api_client.py b/app/notify_client/organizations_api_client.py index 10d121a41..e7cca58ef 100644 --- a/app/notify_client/organizations_api_client.py +++ b/app/notify_client/organizations_api_client.py @@ -78,5 +78,10 @@ class OrganizationsClient(NotifyAdminAPIClient): params={"year": str(year)}, ) + def get_organization_message_usage(self, org_id): + return self.get( + url="/organizations/{}/message-allowance".format(org_id), + ) + organizations_client = OrganizationsClient() diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index 3b238acbf..97d94cae4 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -561,11 +561,6 @@ class ServiceAPIClient(NotifyAdminAPIClient): url="service/get-service-message-ratio?service_id={0}".format(service_id), ) - def get_organization_message_usage(self, org_id): - return self.get( - url="/organizations/{0}/message-allowance".format(org_id), - ) - service_api_client = ServiceAPIClient() diff --git a/package-lock.json b/package-lock.json index cd702f4e1..516b982cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "0.0.1", "license": "CC0", "dependencies": { - "@rollup/plugin-commonjs": "^28.0.7", + "@rollup/plugin-commonjs": "^28.0.8", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/stream": "^3.0.1", "@uswds/uswds": "^3.13.0", "cbor-js": "0.1.0", "d3": "^7.9.0", "jquery": "3.7.1", - "playwright": "^1.56.0", + "playwright": "^1.56.1", "python": "^0.0.4", "query-command-supported": "1.0.0", "sass-embedded": "^1.93.2", @@ -3275,9 +3275,9 @@ } }, "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.7", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.7.tgz", - "integrity": "sha512-6cE2Wr/MkpdtTS8gXlCn9Zdmf7e9Xm96yFqOwFEXuvYLAHtjRf57/n6GEVF4K8NSesT1eKdBtcDA/SQdpW/8nA==", + "version": "28.0.8", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.8.tgz", + "integrity": "sha512-o1Ug9PxYsF61R7/NXO/GgMZZproLd/WH2XA53Tp9ppf6bU1lMlTtC/gUM6zM3mesi2E0rypk+PNtVrELREyWEQ==", "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -10883,12 +10883,12 @@ } }, "node_modules/playwright": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz", - "integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0" + "playwright-core": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -10901,9 +10901,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0.tgz", - "integrity": "sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" diff --git a/package.json b/package.json index 54e9e98dc..d83d14cdb 100644 --- a/package.json +++ b/package.json @@ -30,14 +30,14 @@ "graceful-fs": "^4.2.11" }, "dependencies": { - "@rollup/plugin-commonjs": "^28.0.7", + "@rollup/plugin-commonjs": "^28.0.8", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/stream": "^3.0.1", "@uswds/uswds": "^3.13.0", "cbor-js": "0.1.0", "d3": "^7.9.0", "jquery": "3.7.1", - "playwright": "^1.56.0", + "playwright": "^1.56.1", "python": "^0.0.4", "query-command-supported": "1.0.0", "sass-embedded": "^1.93.2", diff --git a/poetry.lock b/poetry.lock index b5bcc2cce..a2bb2e062 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "ago" @@ -1110,14 +1110,14 @@ pycodestyle = "*" [[package]] name = "flake8-pytest-style" -version = "2.1.0" +version = "2.2.0" description = "A flake8 plugin checking common style issues or inconsistencies with pytest-based tests." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "flake8_pytest_style-2.1.0-py3-none-any.whl", hash = "sha256:a0d6dddcd533bfc13f19b8445907be0330c5e6ccf7090bcd9d5fa5a0b1b65e71"}, - {file = "flake8_pytest_style-2.1.0.tar.gz", hash = "sha256:fee6befdb5915d600ef24e38d48a077d0dcffb032945ae0169486e7ff8a1079a"}, + {file = "flake8_pytest_style-2.2.0-py3-none-any.whl", hash = "sha256:d01c4198a6c4e0ab759a92a0fa7710f10d83ec28e32a50ab6fb2e10f973a2f36"}, + {file = "flake8_pytest_style-2.2.0.tar.gz", hash = "sha256:d23a33294bccfb9f1b11aaf5212256727b299b9c9b17cf21e230c52c1095a468"}, ] [package.dependencies] @@ -1490,14 +1490,14 @@ tests = ["freezegun", "pytest", "pytest-cov"] [[package]] name = "hypothesis" -version = "6.142.0" +version = "6.142.2" description = "A library for property-based testing" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "hypothesis-6.142.0-py3-none-any.whl", hash = "sha256:2d1c3096ee625f327134a693d8e5a48134e3a6cd0914cd0497adb2699bd998b4"}, - {file = "hypothesis-6.142.0.tar.gz", hash = "sha256:584bb5bddb95cfae69151466215433b5620bd7498876e6f6831c9f119344fb0f"}, + {file = "hypothesis-6.142.2-py3-none-any.whl", hash = "sha256:cc6c6e66c06aff695dd255501a767b528e00d84ce3572160425a9ba5e4a47845"}, + {file = "hypothesis-6.142.2.tar.gz", hash = "sha256:c4204a2ce327e45fbaf83a2b58142a285135698dc1d08e368ae9901f06b49e64"}, ] [package.dependencies] @@ -2129,14 +2129,14 @@ files = [ [[package]] name = "moto" -version = "5.1.14" +version = "5.1.15" description = "A library that allows you to easily mock out tests based on AWS infrastructure" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "moto-5.1.14-py3-none-any.whl", hash = "sha256:b9767848953beaf6650f1fd91615a3bcef84d93bd00603fa64dae38c656548e8"}, - {file = "moto-5.1.14.tar.gz", hash = "sha256:450690abb0b152fea7f93e497ac2172f15d8a838b15f22b514db801a6b857ae4"}, + {file = "moto-5.1.15-py3-none-any.whl", hash = "sha256:0ffcf943f421bc6e7248889c7c44182a9ec26f8df3457cd4b52418dab176a720"}, + {file = "moto-5.1.15.tar.gz", hash = "sha256:2ad9cc9710a3460505511543dba6761c8bd2006a49954ad3988bbf20ce9e6413"}, ] [package.dependencies] @@ -4420,4 +4420,4 @@ cffi = ["cffi (>=1.17) ; python_version >= \"3.13\" and platform_python_implemen [metadata] lock-version = "2.1" python-versions = "^3.13.2" -content-hash = "5a3442eedbd6a2a2f9d6fda0452e04ea18463dd794135ae8b0bd601089d43b17" +content-hash = "14ec24e06f09b708ed6d8632b47af2e2c622443d24dec95c8a7d2e4e9675621b" diff --git a/pyproject.toml b/pyproject.toml index 17cc84a29..a214c5316 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ flask-login = "^0.6" flask-talisman = "*" flask-wtf = "^1.2" gunicorn = {version = "==23.0.0", extras = ["gevent"]} -hypothesis = "^6.142.0" +hypothesis = "^6.142.2" humanize = "~=4.14" itsdangerous = "~=2.2" jinja2 = "^3.1.6" @@ -83,7 +83,7 @@ freezegun = "^1.5.5" flake8 = "^7.3.0" flake8-bugbear = "^24.12.12" flake8-print = "^5.0.0" -flake8-pytest-style = "^2.1.0" +flake8-pytest-style = "^2.2.0" isort = "^7.0.0" jinja2-cli = {version = "==0.8.2", extras = ["yaml"]} moto = "*" diff --git a/terraform/demo/providers.tf b/terraform/demo/providers.tf index 3817eae91..a35684a9a 100644 --- a/terraform/demo/providers.tf +++ b/terraform/demo/providers.tf @@ -2,6 +2,10 @@ terraform { required_version = "~> 1.7" required_providers { cloudfoundry = { + source = "cloudfoundry/cloudfoundry" + version = "1.9.0" + } + cfcommunity = { source = "cloudfoundry-community/cloudfoundry" version = "0.53.1" } @@ -16,7 +20,16 @@ terraform { } } +# Official provider (should be default but aliased for now) provider "cloudfoundry" { + alias = "official" + api_url = "https://api.fr.cloud.gov" + user = var.cf_user + password = var.cf_password +} + +# Community provider (should be aliased but default for now) +provider "cfcommunity" { api_url = "https://api.fr.cloud.gov" user = var.cf_user password = var.cf_password diff --git a/terraform/production/providers.tf b/terraform/production/providers.tf index 8afef6078..6f0834742 100644 --- a/terraform/production/providers.tf +++ b/terraform/production/providers.tf @@ -2,6 +2,10 @@ terraform { required_version = "~> 1.7" required_providers { cloudfoundry = { + source = "cloudfoundry/cloudfoundry" + version = "1.9.0" + } + cfcommunity = { source = "cloudfoundry-community/cloudfoundry" version = "0.53.1" } @@ -16,7 +20,16 @@ terraform { } } +# Official provider (should be default but aliased for now) provider "cloudfoundry" { + alias = "official" + api_url = "https://api.fr.cloud.gov" + user = var.cf_user + password = var.cf_password +} + +# Community provider (should be aliased but default for now) +provider "cfcommunity" { api_url = "https://api.fr.cloud.gov" user = var.cf_user password = var.cf_password diff --git a/terraform/sandbox/providers.tf b/terraform/sandbox/providers.tf index 92ff186b8..247c47b6c 100644 --- a/terraform/sandbox/providers.tf +++ b/terraform/sandbox/providers.tf @@ -2,6 +2,10 @@ terraform { required_version = "~> 1.7" required_providers { cloudfoundry = { + source = "cloudfoundry/cloudfoundry" + version = "1.9.0" + } + cfcommunity = { source = "cloudfoundry-community/cloudfoundry" version = "0.53.1" } @@ -16,7 +20,16 @@ terraform { } } +# Official provider (should be default but aliased for now) provider "cloudfoundry" { + alias = "official" + api_url = "https://api.fr.cloud.gov" + user = var.cf_user + password = var.cf_password +} + +# Community provider (should be aliased but default for now) +provider "cfcommunity" { api_url = "https://api.fr.cloud.gov" user = var.cf_user password = var.cf_password diff --git a/terraform/staging/main.tf b/terraform/staging/main.tf index 87c1e64dd..274be3206 100644 --- a/terraform/staging/main.tf +++ b/terraform/staging/main.tf @@ -26,6 +26,35 @@ module "redis-v70" { ) } +data "cloudfoundry_space" "space" { + provider = cloudfoundry.official + org = "9e428562-a2d9-41b4-9c23-1ef5237fb44e" + name = local.cf_space_name +} + + +# IF we want to upgrade from cloudfoundry communit to official, +# we need to do something like what's commented out below +# +# We also need to run +# +# terraform state replace-provider \ +# -auto-approve \ +# -state=terraform.tfstate +# 'registry.terraform.io/cloudfoundry-community/cloudfoundry' \ +# 'registry.terraform.io/cloudfoundry/cloudfoundry' \ +# module.logo_upload_bucket (or something like that) +# module "logo_upload_bucket" { +# source = "github.com/GSA-TTS/terraform-cloudgov//s3?ref=v2.4.0" +# Right now the default is cfcommunity, remove this when default is cloudfoundry +# providers = { +# cloudfoundry = cloudfoundry.official +# } +# cf_space_id = data.cloudfoundry_space.space.id +# name = "${local.app_name}-logo-upload-bucket-${local.env}" +# } + + module "logo_upload_bucket" { source = "github.com/GSA-TTS/terraform-cloudgov//s3?ref=v1.0.0" @@ -34,6 +63,7 @@ module "logo_upload_bucket" { name = "${local.app_name}-logo-upload-bucket-${local.env}" } + module "api_network_route" { source = "../shared/container_networking" diff --git a/terraform/staging/providers.tf b/terraform/staging/providers.tf index 95d1edaa5..e14251fdf 100644 --- a/terraform/staging/providers.tf +++ b/terraform/staging/providers.tf @@ -2,6 +2,10 @@ terraform { required_version = "~> 1.7" required_providers { cloudfoundry = { + source = "cloudfoundry/cloudfoundry" + version = "1.9.0" + } + cfcommunity = { source = "cloudfoundry-community/cloudfoundry" version = "0.53.1" } @@ -16,7 +20,16 @@ terraform { } } +# Official provider (should be default but aliased for now) provider "cloudfoundry" { + alias = "official" + api_url = "https://api.fr.cloud.gov" + user = var.cf_user + password = var.cf_password +} + +# Community provider (should be aliased but default for now) +provider "cfcommunity" { api_url = "https://api.fr.cloud.gov" user = var.cf_user password = var.cf_password diff --git a/tests/app/main/views/organizations/test_organizations.py b/tests/app/main/views/organizations/test_organizations.py index 857ffa5da..3dc7e74d8 100644 --- a/tests/app/main/views/organizations/test_organizations.py +++ b/tests/app/main/views/organizations/test_organizations.py @@ -655,16 +655,12 @@ def test_organization_trial_mode_services_shows_all_non_live_services( ) services = page.select(".browse-list-item") - assert len(services) == 2 + assert len(services) == 1 assert normalize_spaces(services[0].text) == "2" - assert normalize_spaces(services[1].text) == "3" assert services[0].find("a")["href"] == url_for( "main.service_dashboard", service_id="2" ) - assert services[1].find("a")["href"] == url_for( - "main.service_dashboard", service_id="3" - ) def test_organization_trial_mode_services_doesnt_work_if_not_platform_admin( @@ -1531,7 +1527,7 @@ def test_organization_dashboard_shows_message_usage( active_user_with_permissions, ): mock_message_usage = mocker.patch( - "app.service_api_client.get_organization_message_usage", + "app.organizations_client.get_organization_message_usage", return_value={ "messages_sent": 1000, "messages_remaining": 2000, @@ -1542,7 +1538,6 @@ def test_organization_dashboard_shows_message_usage( "app.organizations_client.get_organization_services", return_value=[], ) - client_request.login(active_user_with_permissions) page = client_request.get( ".organization_dashboard", @@ -1566,7 +1561,7 @@ def test_organization_dashboard_shows_service_counts( active_user_with_permissions, ): mocker.patch( - "app.service_api_client.get_organization_message_usage", + "app.organizations_client.get_organization_message_usage", return_value={ "messages_sent": 0, "messages_remaining": 0,