Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .ds.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
"filename": "app/config.py",
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
"is_verified": false,
"line_number": 118,
"line_number": 127,
"is_secret": false
}
],
Expand Down Expand Up @@ -634,5 +634,5 @@
}
]
},
"generated_at": "2025-10-07T17:43:26Z"
"generated_at": "2025-10-14T19:59:45Z"
}
9 changes: 9 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ class Config(object):
# TODO: reassign this
NOTIFY_SERVICE_ID = "d6aa2c68-a2d9-4437-ab19-3ae8eb202553"

ORGANIZATION_DASHBOARD_ENABLED = (
getenv("ORGANIZATION_DASHBOARD_ENABLED", "False") == "True"
)

NOTIFY_BILLING_DETAILS = json.loads(getenv("NOTIFY_BILLING_DETAILS") or "null") or {
"account_number": "98765432",
"sort_code": "01-23-45",
Expand Down Expand Up @@ -109,6 +113,11 @@ class Development(Config):
ASSET_PATH = "/static/"
NOTIFY_LOG_LEVEL = "DEBUG"

# Feature Flags
ORGANIZATION_DASHBOARD_ENABLED = (
getenv("ORGANIZATION_DASHBOARD_ENABLED", "True") == "True"
)

# Buckets
CSV_UPLOAD_BUCKET = _s3_credentials_from_env("CSV")
LOGO_UPLOAD_BUCKET = _s3_credentials_from_env("LOGO")
Expand Down
43 changes: 25 additions & 18 deletions app/main/views/organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import datetime
from functools import partial

from flask import flash, redirect, render_template, request, url_for
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
Expand Down Expand Up @@ -65,13 +65,29 @@ def add_organization():
@main.route("/organizations/<uuid:org_id>", methods=["GET"])
@user_has_permissions()
def organization_dashboard(org_id):
if not current_app.config.get("ORGANIZATION_DASHBOARD_ENABLED", False):
return redirect(url_for(".organization_usage", org_id=org_id))

year = requested_and_current_financial_year(request)[0]

# TODO: total message allowance
return render_template(
"views/organizations/organization/index.html",
selected_year=year,
)


@main.route("/organizations/<uuid:org_id>/usage", methods=["GET"])
@user_has_permissions()
def organization_usage(org_id):
year, current_financial_year = requested_and_current_financial_year(request)
services = current_organization.services_and_usage(financial_year=year)["services"]

return render_template(
"views/organizations/organization/index.html",
"views/organizations/organization/usage.html",
services=services,
years=get_tuples_of_financial_years(
partial(url_for, ".organization_dashboard", org_id=current_organization.id),
partial(url_for, ".organization_usage", org_id=current_organization.id),
start=current_financial_year - 2,
end=current_financial_year,
),
Expand All @@ -90,14 +106,10 @@ def organization_dashboard(org_id):
@main.route("/organizations/<uuid:org_id>/download-usage-report.csv", methods=["GET"])
@user_has_permissions()
def download_organization_usage_report(org_id):
selected_year_input = request.args.get("selected_year")
# Validate selected_year to prevent header injection
if (
selected_year_input
and selected_year_input.isdigit()
and len(selected_year_input) == 4
):
selected_year = selected_year_input
# Validate and sanitize selected_year to prevent header injection
selected_year_input = request.args.get("selected_year", "")
if selected_year_input.isdigit() and len(selected_year_input) == 4:
selected_year = str(int(selected_year_input))
else:
selected_year = str(datetime.now().year)
services_usage = current_organization.services_and_usage(
Expand Down Expand Up @@ -142,13 +154,8 @@ def download_organization_usage_report(org_id):
{
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": (
"inline;"
'filename="{} organization usage report for year {}'
' - generated on {}.csv"'.format(
safe_org_name,
selected_year,
datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
)
f'inline;filename="{safe_org_name} organization usage report for year {selected_year}'
f' - generated on {datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")}.csv"'
),
},
)
Expand Down
3 changes: 3 additions & 0 deletions app/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,9 @@ class OrgNavigation(Navigation):
"dashboard": {
"organization_dashboard",
},
"usage": {
"organization_usage",
},
"settings": {
"edit_organization_billing_details",
"edit_organization_domains",
Expand Down
5 changes: 4 additions & 1 deletion app/templates/components/org_nav.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<nav id="nav-org-nav" aria-label="Organization navigation" class="nav margin-bottom-4">
<ul class="usa-sidenav">
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('dashboard') }}" href="{{ url_for('.organization_dashboard', org_id=current_org.id) }}">Usage</a></li>
{% if config.ORGANIZATION_DASHBOARD_ENABLED %}
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('dashboard') }}" href="{{ url_for('.organization_dashboard', org_id=current_org.id) }}">Dashboard</a></li>
{% endif %}
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('usage') }}" href="{{ url_for('.organization_usage', org_id=current_org.id) }}">Usage</a></li>
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_org_users', org_id=current_org.id) }}">Team members</a></li>
{% if current_user.platform_admin %}
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('settings') }}" href="{{ url_for('.organization_settings', org_id=current_org.id) }}">Settings</a></li>
Expand Down
105 changes: 15 additions & 90 deletions app/templates/views/organizations/organization/index.html
Original file line number Diff line number Diff line change
@@ -1,102 +1,27 @@
{% from "components/page-header.html" import page_header %}
{% from "components/big-number.html" import big_number %}
{% from "components/live-search.html" import live_search %}
{% from "components/pill.html" import pill %}
{% extends "withnav_template.html" %}

{% block org_page_title %}
Usage
Organization Dashboard
{% endblock %}

{% block maincolumn_content %}

{{ page_header('Usage', size='medium') }}
{{ page_header('Organization Dashboard', size='large') }}

<div class="margin-bottom-3">
{{ pill(years, selected_year, big_number_args={'smallest': True}) }}
</div>

<div class="grid-row margin-bottom-3">
<div class="grid-col-6">
<h2 class="font-heading-md">Emails</h2>
<div class="keyline-block">
{{ big_number(
total_emails_sent,
label='sent',
smaller=True
) }}
</div>
</div>
<div class="grid-col-6">
<h2 class="font-heading-md">Text messages</h2>
<div class="keyline-block">
{{ big_number(
total_sms_cost,
'spent',
currency="$",
smaller=True
) }}
</div>
</div>
</div>

{% if search_form %}
<div>
{{ live_search(
target_selector='.organization-service',
show=True,
form=search_form,
label='Search by service'
) }}
</div>
{% endif %}

<h2 class="font-heading-md {% if search_form %}visually-hidden{% endif %}">By service</h2>

{% for service in services %}
<div class="keyline-block organization-service">
<h3 class="live-search-relevant">
<a href="{{ url_for('main.usage', service_id=service.service_id) }}" class="usa-link browse-list-link">{{ service.service_name }}</a>
</h3>
<div class="grid-row">
<div class="grid-col-6">
{{ big_number(
service.emails_sent,
label=service.emails_sent|message_count_label('email'),
smallest=True
) }}
</div>
<div class="grid-col-6">
{% if service.sms_cost %}
{{ big_number(
service.sms_cost,
'spent on text messages',
currency="$",
smallest=True
) }}
{% else %}
{{ big_number(
service.sms_billable_units,
'free {}'.format(service.sms_billable_units|message_count_label('sms')),
smallest=True
) }}
{% endif %}
</div>
</div>
</div>
{% endfor %}
<div class="keyline-block"></div>
{% if not services %}
<p class="usa-body usa-hint">
{{ current_org.name }} has no live services on Notify.gov
<div class="margin-top-5 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:
</p>
<div class="keyline-block"></div>
{% else %}
<div class="js-stick-at-bottom-when-scrolling">
<p>
<a href="{{ download_link }}" download="download" class="usa-link">Download this report (<abbr title="Comma separated values">CSV</abbr>)</a>
</p>
</div>
{% endif %}
<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>

{% endblock %}
103 changes: 103 additions & 0 deletions app/templates/views/organizations/organization/usage.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{% from "components/page-header.html" import page_header %}
{% from "components/big-number.html" import big_number %}
{% from "components/live-search.html" import live_search %}
{% from "components/pill.html" import pill %}
{% from "components/table.html" import list_table, field, row_heading %}
{% extends "withnav_template.html" %}

{% block org_page_title %}
Usage
{% endblock %}

{% block maincolumn_content %}

{{ page_header('Usage', size='medium') }}

<div class="margin-bottom-3">
{{ pill(years, selected_year, big_number_args={'smallest': True}) }}
</div>

<div class="grid-row margin-bottom-3">
<div class="grid-col-6">
<h2 class="font-heading-md">Emails</h2>
<div class="keyline-block">
{{ big_number(
total_emails_sent,
label='sent',
smaller=True
) }}
</div>
</div>
<div class="grid-col-6">
<h2 class="font-heading-md">Text messages</h2>
<div class="keyline-block">
{{ big_number(
total_sms_cost,
'spent',
currency="$",
smaller=True
) }}
</div>
</div>
</div>

{% if search_form %}
<div>
{{ live_search(
target_selector='.organization-service',
show=True,
form=search_form,
label='Search by service'
) }}
</div>
{% endif %}

<h2 class="font-heading-md {% if search_form %}visually-hidden{% endif %}">By service</h2>

{% for service in services %}
<div class="keyline-block organization-service">
<h3 class="live-search-relevant">
<a href="{{ url_for('main.usage', service_id=service.service_id) }}" class="usa-link browse-list-link">{{ service.service_name }}</a>
</h3>
<div class="grid-row">
<div class="grid-col-6">
{{ big_number(
service.emails_sent,
label=service.emails_sent|message_count_label('email'),
smallest=True
) }}
</div>
<div class="grid-col-6">
{% if service.sms_cost %}
{{ big_number(
service.sms_cost,
'spent on text messages',
currency="$",
smallest=True
) }}
{% else %}
{{ big_number(
service.sms_billable_units,
'free {}'.format(service.sms_billable_units|message_count_label('sms')),
smallest=True
) }}
{% endif %}
</div>
</div>
</div>
{% endfor %}
<div class="keyline-block"></div>
{% if not services %}
<p class="usa-body usa-hint">
{{ current_org.name }} has no live services on Notify.gov
</p>
<div class="keyline-block"></div>
{% else %}
<div class="js-stick-at-bottom-when-scrolling">
<p>
<a href="{{ download_link }}" download="download" class="usa-link">Download this report (<abbr title="Comma separated values">CSV</abbr>)</a>
</p>
</div>
{% endif %}

{% endblock %}
1 change: 1 addition & 0 deletions deploy-config/demo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ redis_enabled: 1
nr_agent_id: '1134302465'
nr_app_id: '1083160688'
API_PUBLIC_URL: https://notify-api-demo.app.cloud.gov
ORGANIZATION_DASHBOARD_ENABLED: False
1 change: 1 addition & 0 deletions deploy-config/production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ redis_enabled: 1
nr_agent_id: '1050708682'
nr_app_id: '1050708682'
API_PUBLIC_URL: https://notify-api-production.app.cloud.gov
ORGANIZATION_DASHBOARD_ENABLED: False
1 change: 1 addition & 0 deletions deploy-config/sandbox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ SECRET_KEY: sandbox-notify-secret-key
nr_agent_id: ''
nr_app_id: ''
NR_BROWSER_KEY: ''
ORGANIZATION_DASHBOARD_ENABLED: False
1 change: 1 addition & 0 deletions deploy-config/staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ redis_enabled: 1
nr_agent_id: '1134291385'
nr_app_id: '1031640326'
API_PUBLIC_URL: https://notify-api-staging.app.cloud.gov
ORGANIZATION_DASHBOARD_ENABLED: True
2 changes: 2 additions & 0 deletions manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,5 @@ applications:
LOGIN_DOT_GOV_CERTS_URL: ((LOGIN_DOT_GOV_CERTS_URL))

API_PUBLIC_URL: ((API_PUBLIC_URL))

ORGANIZATION_DASHBOARD_ENABLED: ((ORGANIZATION_DASHBOARD_ENABLED))
Loading