mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-02 12:49:01 -04:00
notify-admin-641 let platform admins export listing of all users (#647)
Co-authored-by: Kenneth Kehl <@kkehl@flexion.us> Co-authored-by: stvnrlly <steven.reilly@gsa.gov>
This commit is contained in:
1
Pipfile
1
Pipfile
@@ -6,6 +6,7 @@ name = "pypi"
|
||||
[packages]
|
||||
ago = "~=0.0.95"
|
||||
blinker = "~=1.4"
|
||||
exceptiongroup = "==1.1.2"
|
||||
fido2 = "~=0.9"
|
||||
flask = "~=2.3"
|
||||
flask-basicauth = "~=0.2"
|
||||
|
||||
56
Pipfile.lock
generated
56
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "b2e8852b009644badb4ecedfd2e3040a91c6082b6794954332edc825b02ec233"
|
||||
"sha256": "d27aff981cc071e27820a959f05da0a7884d9f1a1452f8b5e74bb516b8dae7cb"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -24,14 +24,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.0.95"
|
||||
},
|
||||
"anyio": {
|
||||
"hashes": [
|
||||
"sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780",
|
||||
"sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.7.1"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15",
|
||||
@@ -243,7 +235,7 @@
|
||||
"sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
|
||||
"sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==3.2.0"
|
||||
},
|
||||
"click": {
|
||||
@@ -351,11 +343,11 @@
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
"sha256:46b4052a55b56beea3a3bdd7b30295c292bd6827dd442348bc116f2d35b17f0a",
|
||||
"sha256:758e691dbb454d5ccf4e1b154a19e52847f79e21a42fef17b969144af29a4e6c"
|
||||
"sha256:5b7488477388b8c0b70a8ce93b227c5603bc7b77f1565afe8e729c36c51447d7",
|
||||
"sha256:c33971c79af5be968bb897e95c2448e11a645ee84d93b265ce0b7aabe5dfdca8"
|
||||
],
|
||||
"markers": "python_version >= '3.8' and python_version < '4.0'",
|
||||
"version": "==2.4.0"
|
||||
"version": "==2.4.1"
|
||||
},
|
||||
"docopt": {
|
||||
"hashes": [
|
||||
@@ -383,7 +375,7 @@
|
||||
"sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5",
|
||||
"sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"index": "pypi",
|
||||
"version": "==1.1.2"
|
||||
},
|
||||
"fido2": {
|
||||
@@ -540,22 +532,6 @@
|
||||
"ref": "1299ea9e967a61ae2edebe191082fd169b864c64",
|
||||
"version": "==20.1.0"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
|
||||
"sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.14.0"
|
||||
},
|
||||
"httpcore": {
|
||||
"hashes": [
|
||||
"sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888",
|
||||
"sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"humanize": {
|
||||
"hashes": [
|
||||
"sha256:7ca0e43e870981fa684acb5b062deb307218193bca1a01f2b2676479df849b3a",
|
||||
@@ -1142,14 +1118,6 @@
|
||||
],
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
"sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
|
||||
"sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"texttable": {
|
||||
"hashes": [
|
||||
"sha256:290348fb67f7746931bcdfd55ac7584ecd4e5b0846ab164333f0794b121760f2",
|
||||
@@ -1427,7 +1395,7 @@
|
||||
"sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
|
||||
"sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==3.2.0"
|
||||
},
|
||||
"cryptography": {
|
||||
@@ -1480,7 +1448,7 @@
|
||||
"sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5",
|
||||
"sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"index": "pypi",
|
||||
"version": "==1.1.2"
|
||||
},
|
||||
"execnet": {
|
||||
@@ -1610,7 +1578,7 @@
|
||||
"sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1",
|
||||
"sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"
|
||||
],
|
||||
"markers": "python_full_version >= '3.8.0'",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"markupsafe": {
|
||||
@@ -1813,7 +1781,7 @@
|
||||
"sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526",
|
||||
"sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==32.0.1"
|
||||
},
|
||||
"pluggy": {
|
||||
@@ -1986,7 +1954,7 @@
|
||||
"sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec",
|
||||
"sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==13.4.2"
|
||||
},
|
||||
"s3transfer": {
|
||||
@@ -2033,7 +2001,7 @@
|
||||
"sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d",
|
||||
"sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c"
|
||||
],
|
||||
"markers": "python_full_version >= '3.8.0'",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==5.1.0"
|
||||
},
|
||||
"toml": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import itertools
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
|
||||
@@ -12,6 +13,7 @@ from app import (
|
||||
notification_api_client,
|
||||
platform_stats_api_client,
|
||||
service_api_client,
|
||||
user_api_client,
|
||||
)
|
||||
from app.extensions import redis_client
|
||||
from app.main import main
|
||||
@@ -292,6 +294,32 @@ def get_billing_report():
|
||||
return render_template('views/platform-admin/get-billing-report.html', form=form)
|
||||
|
||||
|
||||
@main.route("/platform-admin/reports/get-users-report", methods=['GET', 'POST'])
|
||||
@user_is_platform_admin
|
||||
def get_users_report():
|
||||
headers = [
|
||||
"name", "services", "platform admin", "permissions", "password changed at",
|
||||
"state"
|
||||
]
|
||||
try:
|
||||
result = user_api_client.get_all_users()
|
||||
|
||||
except HTTPError as e:
|
||||
raise e
|
||||
|
||||
rows = []
|
||||
for r in result:
|
||||
rows.append(_get_user_row(r))
|
||||
if rows:
|
||||
return Spreadsheet.from_rows([headers] + rows).as_csv_data, 200, {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': f'attachment; filename="User Report {datetime.utcnow()}.csv"'
|
||||
}
|
||||
else:
|
||||
flash('No results')
|
||||
return render_template('views/platform-admin/get-users-report.html')
|
||||
|
||||
|
||||
@main.route("/platform-admin/reports/volumes-by-service", methods=['GET', 'POST'])
|
||||
@user_is_platform_admin
|
||||
def get_volumes_by_service():
|
||||
@@ -540,3 +568,40 @@ def format_stats_by_service(services):
|
||||
'created_at': service['created_at'],
|
||||
'active': service['active']
|
||||
}
|
||||
|
||||
|
||||
def _get_user_row(r):
|
||||
|
||||
# [{
|
||||
# 'name': 'Kenneth Kehl',
|
||||
# 'organizations': [],
|
||||
# 'password_changed_at': '2023-07-21 14:12:54.832850', 'permissions': {
|
||||
# '672b8a66-e22e-40f6-b1e5-39cc1c6bf857': ['manage_users', 'manage_templates', 'manage_settings', 'send_texts',
|
||||
# 'send_emails', 'manage_api_keys', 'view_activity']},
|
||||
# 'platform_admin': True, 'services': ['672b8a66-e22e-40f6-b1e5-39cc1c6bf857'], 'state': 'active'}]
|
||||
|
||||
row = []
|
||||
row.append(r['name'])
|
||||
|
||||
service_id_name_lookup = {}
|
||||
services = []
|
||||
for s in r['services']:
|
||||
my_service = service_api_client.get_service(s)
|
||||
service_id_name_lookup[my_service['data']['id']] = my_service['data']['name']
|
||||
services.append(my_service['data']['name'])
|
||||
services = str(services)
|
||||
services = services.replace("[", "")
|
||||
services = services.replace("]", "")
|
||||
row.append(services)
|
||||
row.append(r['platform_admin'])
|
||||
permissions = r['permissions']
|
||||
for k, v in service_id_name_lookup.items():
|
||||
if permissions.get(k):
|
||||
permissions[v] = permissions[k]
|
||||
del permissions[k]
|
||||
|
||||
permissions = json.dumps(permissions, indent=4)
|
||||
row.append(permissions)
|
||||
row.append(r['password_changed_at'])
|
||||
row.append(r['state'])
|
||||
return row
|
||||
|
||||
@@ -95,6 +95,7 @@ class HeaderNavigation(Navigation):
|
||||
'live_services_csv',
|
||||
'notifications_sent_by_service',
|
||||
'get_billing_report',
|
||||
'get_users_report',
|
||||
'get_daily_volumes',
|
||||
'get_daily_sms_provider_volumes',
|
||||
'get_volumes_by_service',
|
||||
|
||||
@@ -154,6 +154,10 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
endpoint = '/organizations/{}/users'.format(org_id)
|
||||
return self.get(endpoint)['data']
|
||||
|
||||
def get_all_users(self):
|
||||
endpoint = '/user'
|
||||
return self.get(endpoint)['data']
|
||||
|
||||
@cache.delete('service-{service_id}')
|
||||
@cache.delete('service-{service_id}-template-folders')
|
||||
@cache.delete('user-{user_id}')
|
||||
|
||||
44
app/templates/views/platform-admin/get-users-report.html
Normal file
44
app/templates/views/platform-admin/get-users-report.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends "views/platform-admin/_base_template.html" %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
{% from "components/table.html" import mapping_table, row, text_field %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Billing Report
|
||||
{% endblock %}
|
||||
|
||||
{% block platform_admin_content %}
|
||||
|
||||
<h1 class="font-body-2xl margin-bottom-3">
|
||||
Users Report
|
||||
</h1>
|
||||
|
||||
{% call form_wrapper() %}
|
||||
{{ form.start_date(param_extensions={"hint": {"text": "Use the format YYYY-MM-DD"}}) }}
|
||||
{{ form.end_date(param_extensions={"hint": {"text": "Use the format YYYY-MM-DD"}}) }}
|
||||
{{ page_footer('Download report (CSV)') }}
|
||||
{% endcall %}
|
||||
|
||||
<h2 class="font-body-lg">
|
||||
Data included in the report
|
||||
</h2>
|
||||
<div class="bottom-gutter-3-2">
|
||||
{% call mapping_table(
|
||||
caption='Descriptions of billing report data',
|
||||
field_headings=['Name', 'Description'],
|
||||
field_headings_visible=True,
|
||||
caption_visible=False
|
||||
) %}
|
||||
{% for column_heading, description in [
|
||||
('sms cost', 'The total cost of text messages sent after a service has used its free allowance.'),
|
||||
('sms chargeable units', 'The number of fragments sent after a service has used its free allowance. This number takes into account the cost multiplier for <a class="govuk-link govuk-link--no-visited-state" href="https://www.notifications.service.gov.uk/pricing#international-numbers">sending international text messages</a>.' | safe),
|
||||
('purchase order number, contact names, contact email addresses and billing reference', 'We add this data manually based on the information we get from services. You can help by adding it to the service settings page.'),
|
||||
] %}
|
||||
{% call row() %}
|
||||
{{ text_field(column_heading) }}
|
||||
{{ text_field(description) }}
|
||||
{% endcall %}
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -28,4 +28,7 @@
|
||||
<p>
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.get_daily_sms_provider_volumes') }}">Daily SMS provider volumes Report</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.get_users_report') }}">Users Report</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1008,6 +1008,46 @@ def test_get_daily_volumes_report_calls_api_and_download_data(
|
||||
)
|
||||
|
||||
|
||||
def test_get_users_report(
|
||||
client_request,
|
||||
platform_admin_user,
|
||||
mocker
|
||||
):
|
||||
mocker.patch(
|
||||
"app.main.views.platform_admin.user_api_client.get_all_users",
|
||||
return_value=[{
|
||||
'name': 'Johnny Sokko',
|
||||
'organizations': [],
|
||||
'password_changed_at': '2023-07-21 14:12:54.832850', 'permissions': {
|
||||
'test service': [
|
||||
'manage_users', 'manage_templates', 'manage_settings', 'send_texts',
|
||||
'send_emails', 'manage_api_keys', 'view_activity']},
|
||||
'platform_admin': True, 'services': ['test service'], 'state': 'active'}
|
||||
]
|
||||
|
||||
|
||||
)
|
||||
|
||||
client_request.login(platform_admin_user)
|
||||
response = client_request.post_response(
|
||||
'main.get_users_report',
|
||||
_data={},
|
||||
_expected_status=200,
|
||||
)
|
||||
|
||||
assert response.content_type == 'text/csv; charset=utf-8'
|
||||
assert 'attachment' in response.headers['Content-Disposition']
|
||||
assert 'filename' in response.headers['Content-Disposition']
|
||||
assert 'User Report' in response.headers['Content-Disposition']
|
||||
|
||||
my_response = response.get_data(as_text=True)
|
||||
|
||||
assert 'Johnny Sokko' in my_response
|
||||
assert 'manage_users' in my_response
|
||||
assert 'test service' in my_response
|
||||
assert 'active' in my_response
|
||||
|
||||
|
||||
def test_get_daily_sms_provider_volumes_report_calls_api_and_download_data(
|
||||
client_request,
|
||||
platform_admin_user,
|
||||
|
||||
@@ -103,6 +103,7 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, {
|
||||
'find_users_by_email',
|
||||
'forgot_password',
|
||||
'get_billing_report',
|
||||
'get_users_report',
|
||||
'get_daily_volumes',
|
||||
'get_daily_sms_provider_volumes',
|
||||
'get_volumes_by_service',
|
||||
|
||||
Reference in New Issue
Block a user