diff --git a/app/main/views/platform_admin.py b/app/main/views/platform_admin.py index 6190ac988..f0b5983b5 100644 --- a/app/main/views/platform_admin.py +++ b/app/main/views/platform_admin.py @@ -1,9 +1,11 @@ +import csv import itertools import json from collections import OrderedDict from datetime import datetime +from io import StringIO -from flask import abort, flash, render_template, request, url_for +from flask import Response, abort, flash, render_template, request, url_for from notifications_python_client.errors import HTTPError from app import ( @@ -70,6 +72,40 @@ def platform_admin(): ) +@main.route("/platform-admin/download-all-users") +@user_is_platform_admin +def download_all_users(): + + # Create a CSV string from the user data + users = user_api_client.get_all_users_detailed() + + if len(users) == 0: + return "No data to download." + + output = StringIO() + header = ["Name", "Email Address", "Phone Number", "Service"] + fieldnames = ["name", "email_address", "mobile_number", "service"] + writer = csv.DictWriter( + output, + fieldnames=fieldnames, + delimiter=",", + ) + # Write custom header + writer.writerow(dict(zip(fieldnames, header))) + for user in users: + user_no_commas = {key: value.replace(",", "") for key, value in user.items()} + if user_no_commas["name"].startswith("e2e"): + continue + writer.writerow(user_no_commas) + csv_data = output.getvalue() + + # Create a direct download response with the CSV data and appropriate headers + response = Response(csv_data, content_type="text/csv; charset=utf-8") + response.headers["Content-Disposition"] = "attachment; filename=users.csv" + + return response + + def is_over_threshold(number, total, threshold): percentage = number / total * 100 if total else 0 return percentage > threshold diff --git a/app/navigation.py b/app/navigation.py index 3ce3b6e62..6ef0907a6 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -123,6 +123,7 @@ class HeaderNavigation(Navigation): "get_billing_report", "get_users_report", "get_daily_volumes", + "download_all_users", "get_daily_sms_provider_volumes", "get_volumes_by_service", "organizations", diff --git a/app/notify_client/user_api_client.py b/app/notify_client/user_api_client.py index 06d6385b2..e47bc4030 100644 --- a/app/notify_client/user_api_client.py +++ b/app/notify_client/user_api_client.py @@ -157,6 +157,10 @@ class UserApiClient(NotifyAdminAPIClient): endpoint = "/user" return self.get(endpoint)["data"] + def get_all_users_detailed(self): + endpoint = "/user/report-all-users" + return self.get(endpoint)["data"] + @cache.delete("service-{service_id}") @cache.delete("service-{service_id}-template-folders") @cache.delete("user-{user_id}") diff --git a/app/templates/views/platform-admin/reports.html b/app/templates/views/platform-admin/reports.html index f22faa728..3b7b32d3a 100644 --- a/app/templates/views/platform-admin/reports.html +++ b/app/templates/views/platform-admin/reports.html @@ -31,4 +31,8 @@
+ + {% endblock %} diff --git a/gulpfile.js b/gulpfile.js index b441f4c2d..d4d6ac81f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -99,10 +99,11 @@ const copyGtmHead = () => { // Task to copy images const copyImages = () => { - return src(paths.src + 'images/**/*') + return src(paths.src + 'images/**/*', { encoding: false }) .pipe(dest(paths.dist + 'images/')); }; + // Configure USWDS paths uswds.settings.version = 3; uswds.paths.dist.css = paths.dist + 'css'; diff --git a/tests/app/main/views/test_platform_admin.py b/tests/app/main/views/test_platform_admin.py index e617b27f8..5124bd81d 100644 --- a/tests/app/main/views/test_platform_admin.py +++ b/tests/app/main/views/test_platform_admin.py @@ -1254,3 +1254,36 @@ def test_get_daily_sms_provider_volumes_report_calls_api_and_download_data( + "80" + "\r\n" ) + + +def test_download_all_users(client_request, platform_admin_user, mocker): + mocker.patch( + "app.main.views.platform_admin.user_api_client.get_all_users_detailed", + return_value=[ + { + "name": "Johnny Sokko", + "email_address": "j_sokko@unicorn.gov", + "mobile_number": "15555555555", + "service": "Emperor, Guillotine, Service", + } + ], + ) + + client_request.login(platform_admin_user) + response = client_request.get_response( + "main.download_all_users", + _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 "users" in response.headers["Content-Disposition"] + + my_response = response.get_data(as_text=True) + + assert "Johnny Sokko" in my_response + assert "Emperor Guillotine Service" in my_response + assert "j_sokko@unicorn.gov" in my_response + assert "15555555555" in my_response diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index d346fc71a..aeee32164 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -68,6 +68,7 @@ EXCLUDED_ENDPOINTS = tuple( "delivery_status_callback", "design_content", "documentation", + "download_all_users", "download_notifications_csv", "download_organization_usage_report", "edit_and_format_messages",