From dc14da9638d8552faa7453be5dce86f3e8b50f02 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen <53159604+heyitsmebev@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:26:00 -0800 Subject: [PATCH] 3110 dashboard design feedback & permission changes (#3142) * update org dashboard * allowing org admins to access serviecs within an org * fix test --- app/main/views/activity.py | 2 +- app/main/views/api_keys.py | 18 ++++---- app/main/views/conversation.py | 8 ++-- app/main/views/dashboard.py | 10 ++--- app/main/views/history.py | 2 +- app/main/views/jobs.py | 12 +++--- app/main/views/manage_users.py | 18 ++++---- app/main/views/notifications.py | 6 +-- app/main/views/organizations.py | 4 +- app/main/views/send.py | 24 +++++------ app/main/views/service_settings.py | 42 +++++++++---------- app/main/views/templates.py | 32 +++++++------- app/main/views/tour.py | 8 ++-- app/main/views/uploads.py | 2 +- app/templates/components/org_nav.html | 2 +- .../organizations/organization/index.html | 12 ++++-- .../organization/users/index.html | 6 +-- .../organization/users/invite-org-user.html | 8 ++-- .../test_organization_invites.py | 2 +- .../views/organizations/test_organizations.py | 8 ++-- 20 files changed, 115 insertions(+), 111 deletions(-) diff --git a/app/main/views/activity.py b/app/main/views/activity.py index 258dcddf8..d664c47f0 100644 --- a/app/main/views/activity.py +++ b/app/main/views/activity.py @@ -123,7 +123,7 @@ def get_filtered_jobs(service_id, page): @main.route("/activity/services/") -@user_has_permissions(ServicePermission.VIEW_ACTIVITY) +@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True) def all_jobs_activity(service_id): service_data_retention_days = 8 page = get_page_from_request() diff --git a/app/main/views/api_keys.py b/app/main/views/api_keys.py index 9040fd3cf..26bdc11b6 100644 --- a/app/main/views/api_keys.py +++ b/app/main/views/api_keys.py @@ -23,7 +23,7 @@ dummy_bearer_token = "bearer_token_set" # nosec B105 - this is not a real token @main.route("/services//api") -@user_has_permissions("manage_api_keys") +@user_has_permissions("manage_api_keys", allow_org_user=True) def api_integration(service_id): callbacks_link = ( ".api_callbacks" @@ -40,7 +40,7 @@ def api_integration(service_id): @main.route("/services//api/documentation") -@user_has_permissions("manage_api_keys") +@user_has_permissions("manage_api_keys", allow_org_user=True) def api_documentation(service_id): return redirect(url_for(".documentation"), code=301) @@ -51,7 +51,7 @@ def api_documentation(service_id): endpoint="old_guest_list", ) @main.route("/services//api/guest-list", methods=["GET", "POST"]) -@user_has_permissions("manage_api_keys") +@user_has_permissions("manage_api_keys", allow_org_user=True) def guest_list(service_id): form = GuestList() if form.validate_on_submit(): @@ -70,7 +70,7 @@ def guest_list(service_id): @main.route("/services//api/keys") -@user_has_permissions("manage_api_keys") +@user_has_permissions("manage_api_keys", allow_org_user=True) def api_keys(service_id): return render_template( "views/api/keys.html", @@ -78,7 +78,7 @@ def api_keys(service_id): @main.route("/services//api/keys/create", methods=["GET", "POST"]) -@user_has_permissions("manage_api_keys", restrict_admin_usage=True) +@user_has_permissions("manage_api_keys", restrict_admin_usage=True, allow_org_user=True) def create_api_key(service_id): form = CreateKeyForm(current_service.api_keys) form.key_type.choices = [ @@ -118,7 +118,7 @@ def create_api_key(service_id): @main.route( "/services//api/keys/revoke/", methods=["GET", "POST"] ) -@user_has_permissions("manage_api_keys") +@user_has_permissions("manage_api_keys", allow_org_user=True) def revoke_api_key(service_id, key_id): key_name = current_service.get_api_key(key_id)["name"] if request.method == "GET": @@ -161,7 +161,7 @@ def check_token_against_dummy_bearer(token): @main.route("/services//api/callbacks", methods=["GET"]) -@user_has_permissions("manage_api_keys") +@user_has_permissions("manage_api_keys", allow_org_user=True) def api_callbacks(service_id): if not current_service.has_permission(ServicePermission.INBOUND_SMS): return redirect(url_for(".delivery_status_callback", service_id=service_id)) @@ -192,7 +192,7 @@ def get_delivery_status_callback_details(): "/services//api/callbacks/delivery-status-callback", methods=["GET", "POST"], ) -@user_has_permissions("manage_api_keys") +@user_has_permissions("manage_api_keys", allow_org_user=True) def delivery_status_callback(service_id): delivery_status_callback = get_delivery_status_callback_details() back_link = ( @@ -259,7 +259,7 @@ def get_received_text_messages_callback(): "/services//api/callbacks/received-text-messages-callback", methods=["GET", "POST"], ) -@user_has_permissions("manage_api_keys") +@user_has_permissions("manage_api_keys", allow_org_user=True) def received_text_messages_callback(service_id): if not current_service.has_permission(ServicePermission.INBOUND_SMS): return redirect(url_for(".api_integration", service_id=service_id)) diff --git a/app/main/views/conversation.py b/app/main/views/conversation.py index 5dd6f317c..4420715e1 100644 --- a/app/main/views/conversation.py +++ b/app/main/views/conversation.py @@ -13,7 +13,7 @@ from notifications_utils.template import SMSPreviewTemplate @main.route("/services//conversation/") -@user_has_permissions(ServicePermission.VIEW_ACTIVITY) +@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True) def conversation(service_id, notification_id): user_number = get_user_number(service_id, notification_id) @@ -31,7 +31,7 @@ def conversation(service_id, notification_id): @main.route("/services//conversation/.json") -@user_has_permissions(ServicePermission.VIEW_ACTIVITY) +@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True) def conversation_updates(service_id, notification_id): return jsonify( get_conversation_partials( @@ -46,7 +46,7 @@ def conversation_updates(service_id, notification_id): @main.route( "/services//conversation//reply-with/from-folder/" ) -@user_has_permissions(ServicePermission.SEND_MESSAGES) +@user_has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True) def conversation_reply( service_id, notification_id, @@ -70,7 +70,7 @@ def conversation_reply( @main.route( "/services//conversation//reply-with/" ) -@user_has_permissions(ServicePermission.SEND_MESSAGES) +@user_has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True) def conversation_reply_with_template( service_id, notification_id, diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index d84727569..cdb432b6a 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -24,13 +24,13 @@ from app.utils.user import user_has_permissions @main.route("/services//dashboard") -@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES) +@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES, allow_org_user=True) def old_service_dashboard(service_id): return redirect(url_for(".service_dashboard", service_id=service_id)) @main.route("/services/") -@user_has_permissions() +@user_has_permissions(allow_org_user=True) def service_dashboard(service_id): if session.get("invited_user_id"): @@ -84,7 +84,7 @@ def job_is_finished(job_dict): @main.route("/services//daily-stats.json") -@user_has_permissions() +@user_has_permissions(allow_org_user=True) def get_daily_stats(service_id): date_range = get_stats_date_range() days = date_range["days"] @@ -148,7 +148,7 @@ def get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days): @main.route("/services//daily-stats-by-user.json") -@user_has_permissions() +@user_has_permissions(allow_org_user=True) def get_daily_stats_by_user(service_id): date_range = get_stats_date_range() days = date_range["days"] @@ -166,7 +166,7 @@ def get_daily_stats_by_user(service_id): @main.route("/services//template-usage") -@user_has_permissions(ServicePermission.VIEW_ACTIVITY) +@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True) def template_usage(service_id): year, current_financial_year = requested_and_current_financial_year(request) stats = template_statistics_client.get_monthly_template_usage_for_service( diff --git a/app/main/views/history.py b/app/main/views/history.py index 6995c2830..8007bf99e 100644 --- a/app/main/views/history.py +++ b/app/main/views/history.py @@ -11,7 +11,7 @@ from app.utils.user import user_has_permissions @main.route("/services//history") -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def history(service_id): events = _get_events(current_service.id, request.args.get("selected")) diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index fe3fd74ba..1674c8e05 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -26,7 +26,7 @@ from notifications_utils.template import EmailPreviewTemplate, SMSBodyPreviewTem @main.route("/services//jobs") -@user_has_permissions() +@user_has_permissions(allow_org_user=True) def view_jobs(service_id): return redirect( url_for( @@ -37,7 +37,7 @@ def view_jobs(service_id): @main.route("/services//jobs/") -@user_has_permissions() +@user_has_permissions(allow_org_user=True) def view_job(service_id, job_id): job = Job.from_id(job_id, service_id=current_service.id) if job.cancelled: @@ -83,7 +83,7 @@ def view_job(service_id, job_id): @main.route("/services//jobs/.csv") -@user_has_permissions(ServicePermission.VIEW_ACTIVITY) +@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True) def view_job_csv(service_id, job_id): job = Job.from_id(job_id, service_id=service_id) filter_args = parse_filter_args(request.args) @@ -111,14 +111,14 @@ def view_job_csv(service_id, job_id): @main.route("/services//jobs/", methods=["POST"]) -@user_has_permissions(ServicePermission.SEND_MESSAGES) +@user_has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True) def cancel_job(service_id, job_id): Job.from_id(job_id, service_id=service_id).cancel() return redirect(url_for("main.service_dashboard", service_id=service_id)) @main.route("/services//jobs//status.json") -@user_has_permissions() +@user_has_permissions(allow_org_user=True) def view_job_status_poll(service_id, job_id): from app.notify_client.job_api_client import job_api_client @@ -147,7 +147,7 @@ def view_job_status_poll(service_id, job_id): @main.route("/services//jobs//notifications-table") -@user_has_permissions() +@user_has_permissions(allow_org_user=True) def view_job_notifications_table(service_id, job_id): """Endpoint that returns only the notifications table HTML fragment.""" job = Job.from_id(job_id, service_id=current_service.id) diff --git a/app/main/views/manage_users.py b/app/main/views/manage_users.py index 92cb94982..ff1f071d1 100644 --- a/app/main/views/manage_users.py +++ b/app/main/views/manage_users.py @@ -44,7 +44,7 @@ def manage_users(service_id): @main.route( "/services//users/invite/", methods=["GET", "POST"] ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def invite_user(service_id, user_id=None): form_class = InviteUserForm form = form_class( @@ -116,7 +116,7 @@ def invite_user(service_id, user_id=None): @main.route("/services//users/", methods=["GET", "POST"]) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def edit_user_permissions(service_id, user_id): service_has_email_auth = current_service.has_permission( ServicePermission.EMAIL_AUTH @@ -169,7 +169,7 @@ def edit_user_permissions(service_id, user_id): @main.route("/services//users//delete", methods=["POST"]) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def remove_user_from_service(service_id, user_id): try: service_api_client.remove_user_from_service(service_id, user_id) @@ -192,7 +192,7 @@ def remove_user_from_service(service_id, user_id): "/services//users//edit-email", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def edit_user_email(service_id, user_id): user = current_service.get_team_member(user_id) user_email = user.email_address @@ -225,7 +225,7 @@ def edit_user_email(service_id, user_id): "/services//users//edit-email/confirm", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def confirm_edit_user_email(service_id, user_id): user = current_service.get_team_member(user_id) session_key = "team_member_email_change-{}".format(user_id) @@ -263,7 +263,7 @@ def confirm_edit_user_email(service_id, user_id): "/services//users//edit-mobile-number", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def edit_user_mobile_number(service_id, user_id): user = current_service.get_team_member(user_id) user_mobile_number = redact_mobile_number(user.mobile_number) @@ -293,7 +293,7 @@ def edit_user_mobile_number(service_id, user_id): "/services//users//edit-mobile-number/confirm", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def confirm_edit_user_mobile_number(service_id, user_id): user = current_service.get_team_member(user_id) if "team_member_mobile_change" in session: @@ -331,7 +331,7 @@ def confirm_edit_user_mobile_number(service_id, user_id): "/services//cancel-invited-user/", methods=["GET"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def cancel_invited_user(service_id, invited_user_id): current_service.cancel_invite(invited_user_id) @@ -350,7 +350,7 @@ def cancel_invited_user(service_id, invited_user_id): "/services//resend-invite/", methods=["GET"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def resend_invite(service_id, invited_user_id): current_service.resend_invite(invited_user_id) diff --git a/app/main/views/notifications.py b/app/main/views/notifications.py index 20d5dcc17..a3cca0b61 100644 --- a/app/main/views/notifications.py +++ b/app/main/views/notifications.py @@ -35,7 +35,7 @@ from notifications_utils.s3 import S3ObjectNotFound @main.route("/services//notification/") -@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES) +@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES, allow_org_user=True) def view_notification(service_id, notification_id, error_message=None): if error_message: flash(error_message) @@ -108,7 +108,7 @@ def view_notification(service_id, notification_id, error_message=None): @main.route("/services//notification/.json") -@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES) +@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES, allow_org_user=True) def view_notification_updates(service_id, notification_id): return jsonify( **get_single_notification_partials( @@ -149,7 +149,7 @@ PERIOD_TO_S3_FILENAME = { @main.route("/services//download-notifications.csv") -@user_has_permissions(ServicePermission.VIEW_ACTIVITY) +@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True) def download_notifications_csv(service_id): set_timezone() filter_args = parse_filter_args(request.args) diff --git a/app/main/views/organizations.py b/app/main/views/organizations.py index b678e7079..142617855 100644 --- a/app/main/views/organizations.py +++ b/app/main/views/organizations.py @@ -241,10 +241,10 @@ def get_services_dashboard_data(organization, year): 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})" + f"{sms_sent:,} sms ({sms_remainder:,} message parts remaining, ${sms_cost:,.2f})" ) else: - usage_parts.append(f"{sms_sent:,} sms ({sms_remainder:,} remaining)") + usage_parts.append(f"{sms_sent:,} sms ({sms_remainder:,} message parts remaining)") service["usage"] = ", ".join(usage_parts) if usage_parts else "No usage" diff --git a/app/main/views/send.py b/app/main/views/send.py index e2d9f28f5..70e2f9803 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -95,7 +95,7 @@ def get_example_csv_rows(template, use_example_as_example=True, submitted_fields @main.route( "/services//send//csv", methods=["GET", "POST"] ) -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def send_messages(service_id, template_id): notification_count = service_api_client.get_notification_count(service_id) remaining_messages = current_service.message_limit - notification_count @@ -189,7 +189,7 @@ def send_messages(service_id, template_id): @main.route("/services//send/.csv", methods=["GET"]) @user_has_permissions( - ServicePermission.SEND_MESSAGES, ServicePermission.MANAGE_TEMPLATES + ServicePermission.SEND_MESSAGES, ServicePermission.MANAGE_TEMPLATES, allow_org_user=True ) def get_example_csv(service_id, template_id): template = get_template( @@ -215,7 +215,7 @@ def get_example_csv(service_id, template_id): "/services//send//set-sender", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def set_sender(service_id, template_id): session["sender_id"] = None redirect_to_one_off = redirect( @@ -344,7 +344,7 @@ def get_sender_details(service_id, template_type): @main.route("/services//send//one-off") -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def send_one_off(service_id, template_id): session["recipient"] = None session["placeholders"] = {} @@ -388,7 +388,7 @@ def get_notification_check_endpoint(service_id, template): "/services//send//one-off/step-", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def send_one_off_step(service_id, template_id, step_index): if {"recipient", "placeholders"} - set(session.keys()): return redirect( @@ -650,7 +650,7 @@ def _check_messages(service_id, template_id, upload_id, preview_row, **kwargs): "/services///check//row-", methods=["GET"], ) -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def check_messages(service_id, template_id, upload_id, row_index=2): data = _check_messages(service_id, template_id, upload_id, row_index) data["allowed_file_extensions"] = Spreadsheet.ALLOWED_FILE_EXTENSIONS @@ -694,7 +694,7 @@ def check_messages(service_id, template_id, upload_id, row_index=2): "/services///check//preview/row-", methods=["POST"], ) -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def preview_job(service_id, template_id, upload_id, row_index=2): session["scheduled_for"] = request.form.get("scheduled_for", "") data = _check_messages( @@ -709,7 +709,7 @@ def preview_job(service_id, template_id, upload_id, row_index=2): @main.route("/services//start-job/", methods=["POST"]) -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def start_job(service_id, upload_id): scheduled_for = session.pop("scheduled_for", None) job_api_client.create_job( @@ -862,7 +862,7 @@ def get_skip_link(step_index, template): "/services//template//one-off/send-to-myself", methods=["GET"], ) -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def send_one_off_to_myself(service_id, template_id): current_app.logger.info("Send one off to myself") try: @@ -899,7 +899,7 @@ def send_one_off_to_myself(service_id, template_id): "/services//template//notification/check", methods=["GET"], ) -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def check_notification(service_id, template_id): return render_template( "views/notifications/check.html", @@ -981,7 +981,7 @@ def get_template_error_dict(exception): "/services//template//notification/check/preview", methods=["POST"], ) -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def preview_notification(service_id, template_id): recipient = get_recipient() if not recipient: @@ -1013,7 +1013,7 @@ def preview_notification(service_id, template_id): "/services//template//notification/check", methods=["POST"], ) -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def send_notification(service_id, template_id): recipient = get_recipient() diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index a940ea5ef..198b2405b 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -70,7 +70,7 @@ PLATFORM_ADMIN_SERVICE_PERMISSIONS = OrderedDict( @main.route("/services//service-settings") -@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys") +@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys", allow_org_user=True) def service_settings(service_id): return render_template( "views/service-settings.html", @@ -81,7 +81,7 @@ def service_settings(service_id): @main.route( "/services//service-settings/name", methods=["GET", "POST"] ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_name_change(service_id): form = RenameServiceForm(name=current_service.name) @@ -195,7 +195,7 @@ def service_set_permission(service_id, permission): @main.route( "/services//service-settings/archive", methods=["GET", "POST"] ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def archive_service(service_id): if not current_service.active or not ( current_service.trial_mode or current_user.platform_admin @@ -268,7 +268,7 @@ def resume_service(service_id): "/services//service-settings/send-files-by-email", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def send_files_by_email_contact_details(service_id): form = ServiceContactDetailsForm() contact_details = None @@ -298,7 +298,7 @@ def send_files_by_email_contact_details(service_id): @main.route( "/services//service-settings/set-reply-to-email", methods=["GET"] ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_set_reply_to_email(service_id): return redirect(url_for(".service_email_reply_to", service_id=service_id)) @@ -306,7 +306,7 @@ def service_set_reply_to_email(service_id): @main.route( "/services//service-settings/email-reply-to", methods=["GET"] ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys") +@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys", allow_org_user=True) def service_email_reply_to(service_id): return render_template("views/service-settings/email_reply_to.html") @@ -315,7 +315,7 @@ def service_email_reply_to(service_id): "/services//service-settings/email-reply-to/add", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_add_email_reply_to(service_id): form = ServiceReplyToEmailForm() first_email_address = current_service.count_email_reply_to_addresses == 0 @@ -359,7 +359,7 @@ def service_add_email_reply_to(service_id): "/services//service-settings/email-reply-to//verify", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_verify_reply_to_address(service_id, notification_id): replace = request.args.get("replace", False) is_default = request.args.get("is_default", False) @@ -379,7 +379,7 @@ def service_verify_reply_to_address(service_id, notification_id): @main.route( "/services//service-settings/email-reply-to//verify.json" ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_verify_reply_to_address_updates(service_id, notification_id): return jsonify( **get_service_verify_reply_to_address_partials(service_id, notification_id) @@ -456,7 +456,7 @@ def get_service_verify_reply_to_address_partials(service_id, notification_id): methods=["GET"], endpoint="service_confirm_delete_email_reply_to", ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_edit_email_reply_to(service_id, reply_to_email_id): form = ServiceReplyToEmailForm() reply_to_email_address = current_service.get_email_reply_to_address( @@ -525,7 +525,7 @@ def service_edit_email_reply_to(service_id, reply_to_email_id): "/services//service-settings/email-reply-to//delete", methods=["POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_delete_email_reply_to(service_id, reply_to_email_id): service_api_client.delete_reply_to_email_address( service_id=current_service.id, @@ -538,7 +538,7 @@ def service_delete_email_reply_to(service_id, reply_to_email_id): "/services//service-settings/set-inbound-number", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_set_inbound_number(service_id): available_inbound_numbers = ( inbound_number_client.get_available_inbound_sms_numbers() @@ -571,7 +571,7 @@ def service_set_inbound_number(service_id): @main.route( "/services//service-settings/sms-prefix", methods=["GET", "POST"] ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_set_sms_prefix(service_id): form = SMSPrefixForm(enabled=current_service.prefix_sms) @@ -590,7 +590,7 @@ def service_set_sms_prefix(service_id): "/services//service-settings/set-international-sms", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_set_international_sms(service_id): form = ServiceOnOffSettingForm( "Send text messages to international phone numbers", @@ -611,7 +611,7 @@ def service_set_international_sms(service_id): @main.route( "/services//service-settings/set-inbound-sms", methods=["GET"] ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_set_inbound_sms(service_id): return render_template( "views/service-settings/set-inbound-sms.html", @@ -622,7 +622,7 @@ def service_set_inbound_sms(service_id): "/services//service-settings/set-", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_set_channel(service_id, channel): if channel not in {"email", "sms"}: abort(404) @@ -647,7 +647,7 @@ def service_set_channel(service_id, channel): @main.route( "/services//service-settings/set-auth-type", methods=["GET"] ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_set_auth_type(service_id): return render_template( "views/service-settings/set-auth-type.html", @@ -655,7 +655,7 @@ def service_set_auth_type(service_id): @main.route("/services//service-settings/sms-sender", methods=["GET"]) -@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys") +@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys", allow_org_user=True) def service_sms_senders(service_id): return render_template( "views/service-settings/sms-senders.html", @@ -666,7 +666,7 @@ def service_sms_senders(service_id): "/services//service-settings/sms-sender/add", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_add_sms_sender(service_id): form = ServiceSmsSenderForm() first_sms_sender = current_service.count_sms_senders == 0 @@ -694,7 +694,7 @@ def service_add_sms_sender(service_id): methods=["GET"], endpoint="service_confirm_delete_sms_sender", ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_edit_sms_sender(service_id, sms_sender_id): sms_sender = current_service.get_sms_sender(sms_sender_id) is_inbound_number = sms_sender["inbound_number_id"] @@ -732,7 +732,7 @@ def service_edit_sms_sender(service_id, sms_sender_id): "/services//service-settings/sms-sender//delete", methods=["POST"], ) -@user_has_permissions(ServicePermission.MANAGE_SERVICE) +@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True) def service_delete_sms_sender(service_id, sms_sender_id): service_api_client.delete_sms_sender( service_id=current_service.id, diff --git a/app/main/views/templates.py b/app/main/views/templates.py index 3a9f791c3..d274090d6 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -40,7 +40,7 @@ form_objects = { @main.route("/services//templates/") -@user_has_permissions() +@user_has_permissions(allow_org_user=True) def view_template(service_id, template_id): template = current_service.get_template(template_id) template_folder = current_service.get_template_folder(template["folder"]) @@ -82,7 +82,7 @@ def view_template(service_id, template_id): "/services//templates//folders/", methods=["GET", "POST"], ) -@user_has_permissions() +@user_has_permissions(allow_org_user=True) def choose_template(service_id, template_type="all", template_folder_id=None): template_folder = current_service.get_template_folder(template_folder_id) @@ -244,7 +244,7 @@ def _view_template_version(service_id, template_id, version): @main.route( "/services//templates//version/" ) -@user_has_permissions() +@user_has_permissions(allow_org_user=True) def view_template_version(service_id, template_id, version): return render_template( "views/templates/template_history.html", @@ -291,7 +291,7 @@ def _add_template_by_type(template_type, template_folder_id): @main.route( "/services//templates/copy/from-service//from-folder/" ) -@user_has_permissions(ServicePermission.MANAGE_TEMPLATES) +@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True) def choose_template_to_copy( service_id, from_service=None, @@ -323,7 +323,7 @@ def choose_template_to_copy( "/services//templates/copy/", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_TEMPLATES) +@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True) def copy_template(service_id, template_id): from_service = request.args.get("from_service") @@ -380,7 +380,7 @@ def _get_template_copy_name(template, existing_templates): "//" ) ) -@user_has_permissions(ServicePermission.MANAGE_TEMPLATES) +@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True) def action_blocked(service_id, notification_type, return_to, template_id=None): back_link = { "add_new_template": partial( @@ -412,7 +412,7 @@ def action_blocked(service_id, notification_type, return_to, template_id=None): "/services//templates/folders//manage", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_TEMPLATES) +@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True) def manage_template_folder(service_id, template_folder_id): template_folder = current_service.get_template_folder_with_user_permission_or_403( template_folder_id, current_user @@ -462,7 +462,7 @@ def manage_template_folder(service_id, template_folder_id): "/services//templates/folders//delete", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_TEMPLATES) +@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True) def delete_template_folder(service_id, template_folder_id): template_folder = current_service.get_template_folder_with_user_permission_or_403( template_folder_id, current_user @@ -527,7 +527,7 @@ def delete_template_folder(service_id, template_folder_id): "/services//templates/folders//add-", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_TEMPLATES) +@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True) def add_service_template(service_id, template_type, template_folder_id=None): if template_type not in current_service.available_template_types: return redirect( @@ -591,7 +591,7 @@ def abort_403_if_not_admin_user(): "/services//templates//edit", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_TEMPLATES) +@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True) def edit_service_template(service_id, template_id): template = current_service.get_template_with_user_permission_or_403( template_id, current_user @@ -679,7 +679,7 @@ def edit_service_template(service_id, template_id): "/services//templates/count--length", methods=["POST"], ) -@user_has_permissions() +@user_has_permissions(allow_org_user=True) def count_content_length(service_id, template_type): if template_type not in {"sms"}: abort(404) @@ -749,7 +749,7 @@ def _get_content_count_error_and_message_for_template(template): "/services//templates//delete", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_TEMPLATES) +@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True) def delete_service_template(service_id, template_id): template = current_service.get_template_with_user_permission_or_403( template_id, current_user @@ -807,7 +807,7 @@ def delete_service_template(service_id, template_id): @main.route( "/services//templates//redact", methods=["GET"] ) -@user_has_permissions(ServicePermission.MANAGE_TEMPLATES) +@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True) def confirm_redact_template(service_id, template_id): template = current_service.get_template_with_user_permission_or_403( template_id, current_user @@ -828,7 +828,7 @@ def confirm_redact_template(service_id, template_id): @main.route( "/services//templates//redact", methods=["POST"] ) -@user_has_permissions(ServicePermission.MANAGE_TEMPLATES) +@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True) def redact_template(service_id, template_id): service_api_client.redact_service_template(service_id, template_id) @@ -847,7 +847,7 @@ def redact_template(service_id, template_id): @main.route("/services//templates//versions") -@user_has_permissions(ServicePermission.VIEW_ACTIVITY) +@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True) def view_template_versions(service_id, template_id): return render_template( "views/templates/choose_history.html", @@ -867,7 +867,7 @@ def view_template_versions(service_id, template_id): "/services//templates//set-template-sender", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.MANAGE_TEMPLATES) +@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True) def set_template_sender(service_id, template_id): template = current_service.get_template_with_user_permission_or_403( template_id, current_user diff --git a/app/main/views/tour.py b/app/main/views/tour.py index 387059709..39509d399 100644 --- a/app/main/views/tour.py +++ b/app/main/views/tour.py @@ -15,7 +15,7 @@ from app.utils.user import user_has_permissions @main.route("/services//tour/") -@user_has_permissions(ServicePermission.SEND_MESSAGES) +@user_has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True) def begin_tour(service_id, template_id): db_template = current_service.get_template_with_user_permission_or_403( template_id, current_user @@ -48,7 +48,7 @@ def begin_tour(service_id, template_id): "/services//tour//step-", methods=["GET", "POST"], ) -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def tour_step(service_id, template_id, step_index): db_template = current_service.get_template_with_user_permission_or_403( template_id, current_user @@ -167,7 +167,7 @@ def _get_tour_step_back_link(service_id, template_id, step_index): @main.route( "/services//tour//check", methods=["GET"] ) -@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) +@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True) def check_tour_notification(service_id, template_id): db_template = current_service.get_template_with_user_permission_or_403( template_id, current_user @@ -224,7 +224,7 @@ def check_tour_notification(service_id, template_id): @main.route("/services//end-tour/") -@user_has_permissions(ServicePermission.MANAGE_TEMPLATES) +@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True) def go_to_dashboard_after_tour(service_id, example_template_id): service_api_client.delete_service_template(service_id, example_template_id) diff --git a/app/main/views/uploads.py b/app/main/views/uploads.py index d8b8ac57c..51a5f46bf 100644 --- a/app/main/views/uploads.py +++ b/app/main/views/uploads.py @@ -11,7 +11,7 @@ MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB @main.route("/services//uploads") -@user_has_permissions() +@user_has_permissions(allow_org_user=True) def uploads(service_id): # No tests have been written, this has been quickly prepared for user research. # It's also very like that a new view will be created to show uploads. diff --git a/app/templates/components/org_nav.html b/app/templates/components/org_nav.html index d93eb8cf7..0e5960b17 100644 --- a/app/templates/components/org_nav.html +++ b/app/templates/components/org_nav.html @@ -4,7 +4,7 @@
  • Dashboard
  • {% endif %}
  • Usage
  • -
  • Team members
  • +
  • Org admins
  • {% if current_user.platform_admin %}
  • Settings
  • Trial mode services
  • diff --git a/app/templates/views/organizations/organization/index.html b/app/templates/views/organizations/organization/index.html index 9150877e6..02eb50e20 100644 --- a/app/templates/views/organizations/organization/index.html +++ b/app/templates/views/organizations/organization/index.html @@ -37,7 +37,7 @@
    {{ live_services }} Live {{ trial_services }} Trial - {{ suspended_services }} Suspended + {{ suspended_services }} Archived
    @@ -140,6 +140,9 @@ {% endif %} +
    @@ -273,7 +277,7 @@ {{ service.name }} {% if not service.active %} - Suspended + Archived {% elif service.restricted %} Trial {% else %} diff --git a/app/templates/views/organizations/organization/users/index.html b/app/templates/views/organizations/organization/users/index.html index c052a6806..8f53039c1 100644 --- a/app/templates/views/organizations/organization/users/index.html +++ b/app/templates/views/organizations/organization/users/index.html @@ -5,7 +5,7 @@ {% from "components/components/button/macro.njk" import usaButton %} {% block org_page_title %} - Team members + Organization administrators {% endblock %} {% block maincolumn_content %} @@ -20,7 +20,7 @@ {% endif %}

    - Team members + Organization administrators

    {% if show_search_box %} @@ -66,7 +66,7 @@
    {{ usaButton({ "element": "a", - "text": "Invite team member", + "text": "Invite an organization administrator", "href": url_for('.invite_org_user', org_id=current_org.id), "classes": "usa-button" }) }} diff --git a/app/templates/views/organizations/organization/users/invite-org-user.html b/app/templates/views/organizations/organization/users/invite-org-user.html index f8a61bc79..6f3508a7f 100644 --- a/app/templates/views/organizations/organization/users/invite-org-user.html +++ b/app/templates/views/organizations/organization/users/invite-org-user.html @@ -15,7 +15,7 @@ {% block maincolumn_content %} - {{ page_header("Invite a team member") }} + {{ page_header("Invite an organization administrator") }} {% call form_wrapper() %} {{ form.email_address( param_extensions={ @@ -26,11 +26,11 @@

    - {{ current_org.name }} team members can: + {{ current_org.name }} organization administrators can:

      -
    • see usage and team members for each service
    • -
    • invite other team members
    • +
    • access and manage all services
    • +
    • invite or remove other organization administrators
    diff --git a/tests/app/main/views/organizations/test_organization_invites.py b/tests/app/main/views/organizations/test_organization_invites.py index 9a4f8fef4..937730738 100644 --- a/tests/app/main/views/organizations/test_organization_invites.py +++ b/tests/app/main/views/organizations/test_organization_invites.py @@ -81,7 +81,7 @@ def test_cancel_invited_org_user_cancels_user_invitations( invited_user_id=sample_org_invite["id"], _follow_redirects=True, ) - assert normalize_spaces(page.h1.text) == "Team members" + assert normalize_spaces(page.h1.text) == "Organization administrators" flash_banner = normalize_spaces( page.select_one(".usa-alert--success .usa-alert__text").text ) diff --git a/tests/app/main/views/organizations/test_organizations.py b/tests/app/main/views/organizations/test_organizations.py index 684828619..b94c6240a 100644 --- a/tests/app/main/views/organizations/test_organizations.py +++ b/tests/app/main/views/organizations/test_organizations.py @@ -839,7 +839,7 @@ def test_edit_organization_user_shows_the_delete_confirmation_banner( user_id=active_user_with_permissions["id"], ) - assert normalize_spaces(page.h1) == "Team members" + assert normalize_spaces(page.h1) == "Organization administrators" banner = page.select_one(".usa-alert--error") banner_text = banner.select_one(".usa-alert__text") @@ -1616,7 +1616,7 @@ def test_organization_dashboard_shows_service_counts( 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) + assert "1 Archived" in normalize_spaces(service_box.text) def test_organization_dashboard_services_table( @@ -1693,7 +1693,7 @@ def test_organization_dashboard_services_table( 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) + assert "249,500 message parts remaining" in normalize_spaces(first_row_cells[2].text) assert normalize_spaces(first_row_cells[3].text) == "N/A" assert normalize_spaces(first_row_cells[4].text) == "Welcome SMS" @@ -1702,7 +1702,7 @@ def test_organization_dashboard_services_table( 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) + assert "249,900 message parts remaining" in normalize_spaces(second_row_cells[2].text) assert normalize_spaces(second_row_cells[3].text) == "N/A" assert normalize_spaces(second_row_cells[4].text) == "Reminder SMS"