Merge pull request #3004 from GSA/2967-calculate-an-organizations-message-usage-for-current-agreement-period

2967 calculate an organization's message usage for current agreement period
This commit is contained in:
ccostino
2025-10-21 10:14:42 -04:00
committed by GitHub
5 changed files with 156 additions and 8 deletions

View File

@@ -62,6 +62,20 @@ def add_organization():
return render_template("views/organizations/add-organization.html", form=form)
def get_organization_message_allowance(org_id):
try:
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 = {}
return {
"messages_sent": message_usage.get("messages_sent", 0),
"messages_remaining": message_usage.get("messages_remaining", 0),
"total_message_limit": message_usage.get("total_message_limit", 0),
}
@main.route("/organizations/<uuid:org_id>", methods=["GET"])
@user_has_permissions()
def organization_dashboard(org_id):
@@ -70,10 +84,16 @@ def organization_dashboard(org_id):
year = requested_and_current_financial_year(request)[0]
# TODO: total message allowance
message_allowance = get_organization_message_allowance(org_id)
return render_template(
"views/organizations/organization/index.html",
selected_year=year,
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,
)

View File

@@ -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):

View File

@@ -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()

View File

@@ -9,7 +9,56 @@
{{ page_header('Organization Dashboard', size='large') }}
<div class="margin-top-5 margin-bottom-5">
<div id="totalMessageChartContainer" data-messages-sent="{{ messages_sent|default(0) }}" data-messages-remaining="{{ messages_remaining|default(0) }}" data-total-message-limit="{{ total_message_limit|default(0) }}">
<div class="grid-row flex-align-center">
<h2 id="chartTitle" class="margin-right-1 margin-y-0">Overall {{ selected_year }} Total Message Allowance</h2>
<button
type="button"
class="usa-tooltip usa-tooltip__information margin-right-0"
data-position="top"
title="Combined totals across all services in {{ current_org.name }}: pending, failed, or delivered"
>
<span class="usa-sr-only">More information</span>
i
</button>
</div>
<svg id="totalMessageChart"></svg>
<div id="message"></div>
</div>
<div id="totalMessageTable" class="margin-bottom-3"></div>
<div class="grid-row margin-bottom-3 maxw-tablet">
<div class="grid-col-12 tablet:grid-col-6 margin-bottom-2 tablet:margin-bottom-0 tablet:padding-right-1">
<div class="usa-summary-box height-full" role="region">
<div class="usa-summary-box__body">
<div class="usa-summary-box__text">
<div class="display-flex flex-align-end">
<div class="flex-1">
<div class="text-base-dark text-uppercase font-sans-3xs text-ls-1 margin-bottom-05">Total Services</div>
<div class="font-sans-xl text-primary-darker line-height-sans-1">{{ total_services }}</div>
</div>
<div class="text-base-dark font-body-2xs line-height-sans-3">
<span class="text-success-dark text-bold">{{ live_services }}</span> Live
<span class="margin-left-1 text-warning-dark text-bold">{{ trial_services }}</span> Trial
<span class="margin-left-1 text-base-dark text-bold">{{ suspended_services }}</span> Suspended
</div>
</div>
</div>
</div>
</div>
</div>
<div class="grid-col-12 tablet:grid-col-6 tablet:padding-left-1">
<div class="usa-summary-box height-full" role="region">
<div class="usa-summary-box__body">
<div class="usa-summary-box__text">
<div class="text-base-dark text-uppercase font-sans-3xs text-ls-1 margin-bottom-05">Agreement Period</div>
<div class="font-sans-xl text-primary-darker">Jan 1 - Dec 31</div>
</div>
</div>
</div>
</div>
</div>
<div class="margin-bottom-5">
<h2 class="font-heading-lg">What is a service?</h2>
<p class="usa-body">
When you join Notify, you're added to a service. This is your organization's workspace for sending text messages and emails. Within your service, you can:

View File

@@ -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(
@@ -1522,3 +1518,77 @@ def test_organization_billing_page_not_accessible_if_not_platform_admin(
client_request.get(
".organization_billing", org_id=ORGANISATION_ID, _expected_status=403
)
def test_organization_dashboard_shows_message_usage(
client_request,
mock_get_organization,
mocker,
active_user_with_permissions,
):
mock_message_usage = mocker.patch(
"app.organizations_client.get_organization_message_usage",
return_value={
"messages_sent": 1000,
"messages_remaining": 2000,
"total_message_limit": 3000,
},
)
mocker.patch(
"app.organizations_client.get_organization_services",
return_value=[],
)
client_request.login(active_user_with_permissions)
page = client_request.get(
".organization_dashboard",
org_id=ORGANISATION_ID,
)
mock_message_usage.assert_called_once_with(ORGANISATION_ID)
assert normalize_spaces(page.select_one("h1").text) == "Organization Dashboard"
chart_container = page.select_one("#totalMessageChartContainer")
assert chart_container["data-messages-sent"] == "1000"
assert chart_container["data-messages-remaining"] == "2000"
assert chart_container["data-total-message-limit"] == "3000"
def test_organization_dashboard_shows_service_counts(
client_request,
mock_get_organization,
mocker,
active_user_with_permissions,
):
mocker.patch(
"app.organizations_client.get_organization_message_usage",
return_value={
"messages_sent": 0,
"messages_remaining": 0,
"total_message_limit": 0,
},
)
mocker.patch(
"app.organizations_client.get_organization_services",
return_value=[
service_json(id_="1", name="Live Service", restricted=False, active=True),
service_json(id_="2", name="Trial Service", restricted=True, active=True),
service_json(id_="3", name="Suspended", restricted=False, active=False),
],
)
client_request.login(active_user_with_permissions)
page = client_request.get(
".organization_dashboard",
org_id=ORGANISATION_ID,
)
summary_boxes = page.select(".usa-summary-box")
assert len(summary_boxes) == 2
service_box = summary_boxes[0]
assert "Total Services" in normalize_spaces(service_box.text)
assert "3" in normalize_spaces(service_box.text)
assert "1 Live" in normalize_spaces(service_box.text)
assert "1 Trial" in normalize_spaces(service_box.text)
assert "1 Suspended" in normalize_spaces(service_box.text)