mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-11 10:28:41 -04:00
Merge branch '3021-create-aggregate-table-of-services-per-org' into 2971-recent-message-activity-per-service
This commit is contained in:
@@ -76,6 +76,40 @@ def get_organization_message_allowance(org_id):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_services_usage(organization, year):
|
||||||
|
|
||||||
|
try:
|
||||||
|
services_and_usage = organization.services_and_usage(financial_year=year, include_all_services=True)["services"]
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error fetching services and usage: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
services = []
|
||||||
|
for service in services_and_usage:
|
||||||
|
service["id"] = service.get("service_id")
|
||||||
|
service["name"] = service.get("service_name")
|
||||||
|
|
||||||
|
emails_sent = service.get("emails_sent", 0)
|
||||||
|
sms_sent = service.get("sms_billable_units", 0)
|
||||||
|
sms_remainder = service.get("sms_remainder", 0)
|
||||||
|
sms_cost = service.get("sms_cost", 0)
|
||||||
|
|
||||||
|
usage_parts = []
|
||||||
|
if emails_sent > 0:
|
||||||
|
usage_parts.append(f"{emails_sent:,} emails")
|
||||||
|
if sms_sent > 0 or sms_remainder > 0:
|
||||||
|
if sms_cost > 0:
|
||||||
|
usage_parts.append(f"{sms_sent:,} sms ({sms_remainder:,} remaining, ${sms_cost:,.2f})")
|
||||||
|
else:
|
||||||
|
usage_parts.append(f"{sms_sent:,} sms ({sms_remainder:,} remaining)")
|
||||||
|
|
||||||
|
service["usage"] = ", ".join(usage_parts) if usage_parts else "No usage"
|
||||||
|
|
||||||
|
services.append(service)
|
||||||
|
|
||||||
|
return services
|
||||||
|
|
||||||
|
|
||||||
@main.route("/organizations/<uuid:org_id>", methods=["GET"])
|
@main.route("/organizations/<uuid:org_id>", methods=["GET"])
|
||||||
@user_has_permissions()
|
@user_has_permissions()
|
||||||
def organization_dashboard(org_id):
|
def organization_dashboard(org_id):
|
||||||
@@ -86,9 +120,12 @@ def organization_dashboard(org_id):
|
|||||||
|
|
||||||
message_allowance = get_organization_message_allowance(org_id)
|
message_allowance = get_organization_message_allowance(org_id)
|
||||||
|
|
||||||
|
services_with_usage = get_services_usage(current_organization, year)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/organizations/organization/index.html",
|
"views/organizations/organization/index.html",
|
||||||
selected_year=year,
|
selected_year=year,
|
||||||
|
services=services_with_usage,
|
||||||
live_services=len(current_organization.live_services),
|
live_services=len(current_organization.live_services),
|
||||||
trial_services=len(current_organization.trial_services),
|
trial_services=len(current_organization.trial_services),
|
||||||
suspended_services=len(current_organization.suspended_services),
|
suspended_services=len(current_organization.suspended_services),
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ class Organization(JSONModel, SortByNameMixin):
|
|||||||
def associate_service(self, service_id):
|
def associate_service(self, service_id):
|
||||||
organizations_client.update_service_organization(service_id, self.id)
|
organizations_client.update_service_organization(service_id, self.id)
|
||||||
|
|
||||||
def services_and_usage(self, financial_year):
|
def services_and_usage(self, financial_year, include_all_services=False):
|
||||||
return organizations_client.get_services_and_usage(self.id, financial_year)
|
return organizations_client.get_services_and_usage(self.id, financial_year, include_all_services)
|
||||||
|
|
||||||
|
|
||||||
class Organizations(SerialisedModelCollection):
|
class Organizations(SerialisedModelCollection):
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ class OrganizationsClient(NotifyAdminAPIClient):
|
|||||||
def remove_user_from_organization(self, org_id, user_id):
|
def remove_user_from_organization(self, org_id, user_id):
|
||||||
return self.delete(f"/organizations/{org_id}/users/{user_id}")
|
return self.delete(f"/organizations/{org_id}/users/{user_id}")
|
||||||
|
|
||||||
def get_services_and_usage(self, org_id, year):
|
def get_services_and_usage(self, org_id, year, include_all_services=False):
|
||||||
return self.get(
|
return self.get(
|
||||||
url=f"/organizations/{org_id}/services-with-usage",
|
url=f"/organizations/{org_id}/services-with-usage",
|
||||||
params={"year": str(year)},
|
params={"year": str(year), "include_all_services": str(include_all_services).lower()},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_organization_message_usage(self, org_id):
|
def get_organization_message_usage(self, org_id):
|
||||||
|
|||||||
@@ -58,19 +58,65 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details class="usa-details">
|
||||||
|
<summary class="usa-details__summary font-heading-lg ">What is a service?</summary>
|
||||||
|
<div class="usa-details__content">
|
||||||
|
<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:
|
||||||
|
</p>
|
||||||
|
<ol class="usa-list">
|
||||||
|
<li>Create and edit message templates</li>
|
||||||
|
<li>Send messages to recipients</li>
|
||||||
|
<li>View message status and history</li>
|
||||||
|
<li>Manage team members and permissions</li>
|
||||||
|
<li>Track usage and delivery statistics</li>
|
||||||
|
<li>If you work for multiple organizations, you may belong to multiple services and can switch between them.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="margin-bottom-5">
|
<div class="margin-bottom-5">
|
||||||
<h2 class="font-heading-lg">What is a service?</h2>
|
<h2 class="font-heading-lg margin-bottom-2">Services Overview</h2>
|
||||||
<p class="usa-body">
|
<div class="table-overflow-x-auto">
|
||||||
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:
|
<table class="usa-table">
|
||||||
</p>
|
<thead>
|
||||||
<ol class="usa-list">
|
<tr>
|
||||||
<li>Create and edit message templates</li>
|
<th scope="col" role="columnheader">Name</th>
|
||||||
<li>Send messages to recipients</li>
|
<th scope="col" role="columnheader">Status</th>
|
||||||
<li>View message status and history</li>
|
<th scope="col" role="columnheader">Usage</th>
|
||||||
<li>Manage team members and permissions</li>
|
<th scope="col" role="columnheader">Primary Contact</th>
|
||||||
<li>Track usage and delivery statistics</li>
|
<th scope="col" role="columnheader">Recent Template Used</th>
|
||||||
<li>If you work for multiple organizations, you may belong to multiple services and can switch between them.</li>
|
</tr>
|
||||||
</ol>
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if services %}
|
||||||
|
{% for service in services %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ url_for('main.service_dashboard', service_id=service.id) }}" class="usa-link">{{ service.name }}</a></td>
|
||||||
|
<td>
|
||||||
|
{% if not service.active %}
|
||||||
|
Suspended
|
||||||
|
{% elif service.restricted %}
|
||||||
|
Trial
|
||||||
|
{% else %}
|
||||||
|
Live
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ service.usage|default('N/A') }}</td>
|
||||||
|
<td>{{ service.primary_contact|default('N/A') }}</td>
|
||||||
|
<td>{{ service.recent_template|default('N/A') }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="table-empty-message">No services found within this organization</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ def test_organization_services_shows_live_services_and_usage(
|
|||||||
|
|
||||||
client_request.login(active_user_with_permissions)
|
client_request.login(active_user_with_permissions)
|
||||||
page = client_request.get(".organization_usage", org_id=ORGANISATION_ID)
|
page = client_request.get(".organization_usage", org_id=ORGANISATION_ID)
|
||||||
mock.assert_called_once_with(ORGANISATION_ID, 2020)
|
mock.assert_called_once_with(ORGANISATION_ID, 2020, False)
|
||||||
|
|
||||||
services = page.select("main h3")
|
services = page.select("main h3")
|
||||||
usage_rows = page.select("main .grid-col-6")
|
usage_rows = page.select("main .grid-col-6")
|
||||||
@@ -447,7 +447,7 @@ def test_organization_services_filters_by_financial_year(
|
|||||||
org_id=ORGANISATION_ID,
|
org_id=ORGANISATION_ID,
|
||||||
year=financial_year,
|
year=financial_year,
|
||||||
)
|
)
|
||||||
mock.assert_called_once_with(ORGANISATION_ID, financial_year)
|
mock.assert_called_once_with(ORGANISATION_ID, financial_year, False)
|
||||||
assert normalize_spaces(page.select_one(".pill").text) == (
|
assert normalize_spaces(page.select_one(".pill").text) == (
|
||||||
"2020 to 2021 fiscal year "
|
"2020 to 2021 fiscal year "
|
||||||
"2019 to 2020 fiscal year "
|
"2019 to 2020 fiscal year "
|
||||||
@@ -1538,6 +1538,11 @@ def test_organization_dashboard_shows_message_usage(
|
|||||||
"app.organizations_client.get_organization_services",
|
"app.organizations_client.get_organization_services",
|
||||||
return_value=[],
|
return_value=[],
|
||||||
)
|
)
|
||||||
|
mocker.patch(
|
||||||
|
"app.organizations_client.get_services_and_usage",
|
||||||
|
return_value={"services": []},
|
||||||
|
)
|
||||||
|
|
||||||
client_request.login(active_user_with_permissions)
|
client_request.login(active_user_with_permissions)
|
||||||
page = client_request.get(
|
page = client_request.get(
|
||||||
".organization_dashboard",
|
".organization_dashboard",
|
||||||
@@ -1576,6 +1581,43 @@ def test_organization_dashboard_shows_service_counts(
|
|||||||
service_json(id_="3", name="Suspended", restricted=False, active=False),
|
service_json(id_="3", name="Suspended", restricted=False, active=False),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
mocker.patch(
|
||||||
|
"app.organizations_client.get_services_and_usage",
|
||||||
|
return_value={
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"service_id": "1",
|
||||||
|
"service_name": "Live Service",
|
||||||
|
"emails_sent": 1000,
|
||||||
|
"sms_billable_units": 300,
|
||||||
|
"sms_remainder": 249700,
|
||||||
|
"sms_cost": 25.50,
|
||||||
|
"active": True,
|
||||||
|
"restricted": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service_id": "2",
|
||||||
|
"service_name": "Trial Service",
|
||||||
|
"emails_sent": 500,
|
||||||
|
"sms_billable_units": 50,
|
||||||
|
"sms_remainder": 249950,
|
||||||
|
"sms_cost": 0,
|
||||||
|
"active": True,
|
||||||
|
"restricted": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service_id": "3",
|
||||||
|
"service_name": "Suspended",
|
||||||
|
"emails_sent": 0,
|
||||||
|
"sms_billable_units": 0,
|
||||||
|
"sms_remainder": 250000,
|
||||||
|
"sms_cost": 0,
|
||||||
|
"active": False,
|
||||||
|
"restricted": False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
client_request.login(active_user_with_permissions)
|
client_request.login(active_user_with_permissions)
|
||||||
page = client_request.get(
|
page = client_request.get(
|
||||||
@@ -1592,3 +1634,79 @@ def test_organization_dashboard_shows_service_counts(
|
|||||||
assert "1 Live" 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 Trial" in normalize_spaces(service_box.text)
|
||||||
assert "1 Suspended" in normalize_spaces(service_box.text)
|
assert "1 Suspended" in normalize_spaces(service_box.text)
|
||||||
|
|
||||||
|
|
||||||
|
def test_organization_dashboard_services_table_shows_usage(
|
||||||
|
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="Service One", restricted=False, active=True),
|
||||||
|
service_json(id_="2", name="Service Two", restricted=True, active=True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
"app.organizations_client.get_services_and_usage",
|
||||||
|
return_value={
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"service_id": "1",
|
||||||
|
"service_name": "Service One",
|
||||||
|
"emails_sent": 1500,
|
||||||
|
"sms_billable_units": 500,
|
||||||
|
"sms_remainder": 249500,
|
||||||
|
"sms_cost": 42.75,
|
||||||
|
"active": True,
|
||||||
|
"restricted": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service_id": "2",
|
||||||
|
"service_name": "Service Two",
|
||||||
|
"emails_sent": 250,
|
||||||
|
"sms_billable_units": 100,
|
||||||
|
"sms_remainder": 249900,
|
||||||
|
"sms_cost": 0,
|
||||||
|
"active": True,
|
||||||
|
"restricted": True,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
client_request.login(active_user_with_permissions)
|
||||||
|
page = client_request.get(
|
||||||
|
".organization_dashboard",
|
||||||
|
org_id=ORGANISATION_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
table = page.select_one("table.usa-table")
|
||||||
|
assert table is not None
|
||||||
|
|
||||||
|
rows = table.select("tbody tr")
|
||||||
|
assert len(rows) == 2
|
||||||
|
|
||||||
|
first_row_cells = rows[0].select("td")
|
||||||
|
assert normalize_spaces(first_row_cells[0].text) == "Service One"
|
||||||
|
assert normalize_spaces(first_row_cells[1].text) == "Live"
|
||||||
|
assert "1,500 emails" in normalize_spaces(first_row_cells[2].text)
|
||||||
|
assert "500 sms" in normalize_spaces(first_row_cells[2].text)
|
||||||
|
assert "249,500 remaining" in normalize_spaces(first_row_cells[2].text)
|
||||||
|
|
||||||
|
second_row_cells = rows[1].select("td")
|
||||||
|
assert normalize_spaces(second_row_cells[0].text) == "Service Two"
|
||||||
|
assert normalize_spaces(second_row_cells[1].text) == "Trial"
|
||||||
|
assert "250 emails" in normalize_spaces(second_row_cells[2].text)
|
||||||
|
assert "100 sms" in normalize_spaces(second_row_cells[2].text)
|
||||||
|
assert "249,900 remaining" in normalize_spaces(second_row_cells[2].text)
|
||||||
|
|||||||
Reference in New Issue
Block a user