fix user permissions save button sometimes deleting

when you hit the delete button, it flashes the delete button and takes
you to the `/service/../user/../delete` url. If you then click the save
button, it would make a POST to the delete URL... and delete the user.

now the page stays on the edit url, but adds a `?delete=yes` query
string. The dangerous flash banner now has an action field which
defines where the browser will make the POST to (which remains at
/delete).
This commit is contained in:
Leo Hemsted
2019-03-14 17:31:51 +00:00
parent 37d12d3aa3
commit f7f9dd8530
5 changed files with 82 additions and 83 deletions

View File

@@ -123,41 +123,43 @@ def edit_user_permissions(service_id, user_id):
user=user, user=user,
form=form, form=form,
service_has_email_auth=service_has_email_auth, service_has_email_auth=service_has_email_auth,
mobile_number=mobile_number mobile_number=mobile_number,
delete=request.args.get('delete'),
) )
@main.route("/services/<service_id>/users/<user_id>/delete", methods=['GET', 'POST']) @main.route("/services/<service_id>/users/<user_id>/delete", methods=['GET'])
@login_required @login_required
@user_has_permissions('manage_service') @user_has_permissions('manage_service')
def remove_user_from_service(service_id, user_id): def remove_user_from_service(service_id, user_id):
user = current_service.get_team_member(user_id) return redirect(url_for(
form = PermissionsForm.from_user(user, service_id) '.edit_user_permissions',
service_id=service_id,
user_id=user_id,
delete='yes'
))
if request.method == 'POST':
try:
service_api_client.remove_user_from_service(service_id, user_id)
except HTTPError as e:
msg = "You cannot remove the only user for a service"
if e.status_code == 400 and msg in e.message:
flash(msg, 'info')
return redirect(url_for(
'.manage_users',
service_id=service_id))
else:
abort(500, e)
return redirect(url_for( @main.route("/services/<service_id>/users/<user_id>/delete", methods=['POST'])
'.manage_users', @login_required
service_id=service_id @user_has_permissions('manage_service')
)) def confirm_remove_user_from_service(service_id, user_id):
try:
service_api_client.remove_user_from_service(service_id, user_id)
except HTTPError as e:
msg = "You cannot remove the only user for a service"
if e.status_code == 400 and msg in e.message:
flash(msg, 'info')
return redirect(url_for(
'.manage_users',
service_id=service_id))
else:
abort(500, e)
flash('Are you sure you want to remove {}?'.format(user.name), 'remove') return redirect(url_for(
return render_template( '.manage_users',
'views/edit-user-permissions.html', service_id=service_id
user=user, ))
form=form
)
@main.route("/services/<service_id>/users/<uuid:user_id>/edit-email", methods=['GET', 'POST']) @main.route("/services/<service_id>/users/<uuid:user_id>/edit-email", methods=['GET', 'POST'])

View File

@@ -212,6 +212,7 @@ class HeaderNavigation(Navigation):
'register_from_org_invite', 'register_from_org_invite',
'registration_continue', 'registration_continue',
'remove_user_from_organisation', 'remove_user_from_organisation',
'confirm_remove_user_from_service',
'remove_user_from_service', 'remove_user_from_service',
'request_letter_branding', 'request_letter_branding',
'request_to_go_live', 'request_to_go_live',
@@ -341,6 +342,7 @@ class MainNavigation(Navigation):
'team-members': { 'team-members': {
'confirm_edit_user_email', 'confirm_edit_user_email',
'confirm_edit_user_mobile_number', 'confirm_edit_user_mobile_number',
'confirm_remove_user_from_service',
'edit_user_email', 'edit_user_email',
'edit_user_mobile_number', 'edit_user_mobile_number',
'edit_user_permissions', 'edit_user_permissions',
@@ -617,6 +619,7 @@ class CaseworkNavigation(Navigation):
'confirm_edit_user_email', 'confirm_edit_user_email',
'confirm_edit_user_mobile_number', 'confirm_edit_user_mobile_number',
'confirm_redact_template', 'confirm_redact_template',
'confirm_remove_user_from_service',
'conversation', 'conversation',
'conversation_reply', 'conversation_reply',
'conversation_reply_with_template', 'conversation_reply_with_template',
@@ -661,17 +664,17 @@ class CaseworkNavigation(Navigation):
'get_notifications_as_json', 'get_notifications_as_json',
'go_to_dashboard_after_tour', 'go_to_dashboard_after_tour',
'inbound_sms_admin', 'inbound_sms_admin',
'inbox',
'inbox_download', 'inbox_download',
'inbox_updates', 'inbox_updates',
'inbox',
'index', 'index',
'information_risk_management', 'information_risk_management',
'information_security', 'information_security',
'integration_testing', 'integration_testing',
'invite_org_user', 'invite_org_user',
'invite_user', 'invite_user',
'letter_branding',
'letter_branding_preview_image', 'letter_branding_preview_image',
'letter_branding',
'letter_template', 'letter_template',
'link_service_to_organisation', 'link_service_to_organisation',
'live_services', 'live_services',
@@ -690,19 +693,19 @@ class CaseworkNavigation(Navigation):
'organisation_preview_letter_branding', 'organisation_preview_letter_branding',
'organisation_settings', 'organisation_settings',
'organisations', 'organisations',
'platform_admin',
'platform_admin_letter_validation_preview', 'platform_admin_letter_validation_preview',
'platform_admin_list_complaints', 'platform_admin_list_complaints',
'platform_admin_returned_letters', 'platform_admin_returned_letters',
'platform_admin',
'pricing', 'pricing',
'privacy', 'privacy',
'public_agreement', 'public_agreement',
'public_download_agreement', 'public_download_agreement',
'received_text_messages_callback', 'received_text_messages_callback',
'redact_template', 'redact_template',
'register',
'register_from_invite', 'register_from_invite',
'register_from_org_invite', 'register_from_org_invite',
'register',
'registration_continue', 'registration_continue',
'remove_user_from_organisation', 'remove_user_from_organisation',
'remove_user_from_service', 'remove_user_from_service',
@@ -952,6 +955,7 @@ class OrgNavigation(Navigation):
'register_from_invite', 'register_from_invite',
'register_from_org_invite', 'register_from_org_invite',
'registration_continue', 'registration_continue',
'confirm_remove_user_from_service',
'remove_user_from_service', 'remove_user_from_service',
'request_letter_branding', 'request_letter_branding',
'request_to_go_live', 'request_to_go_live',

View File

@@ -1,4 +1,6 @@
{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None, context=None) %} {% from "components/form.html" import form_wrapper %}
{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None, context=None, action=None) %}
<div <div
class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}' class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}'
{% if type == 'dangerous' %} {% if type == 'dangerous' %}
@@ -16,14 +18,14 @@
</p> </p>
{% endif %} {% endif %}
{% if delete_button %} {% if delete_button %}
<form method='post'> {% call form_wrapper(action=action) %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" class="button" name="delete">{{ delete_button }}</button> <button type="submit" class="button" name="delete">{{ delete_button }}</button>
</form> {% endcall %}
{% endif %} {% endif %}
</div> </div>
{% endmacro %} {% endmacro %}
{% macro banner_wrapper(type=None, with_tick=False, delete_button=None, subhead=None) %} {% macro banner_wrapper(type=None, with_tick=False, delete_button=None, subhead=None, action=None) %}
{{ banner(caller()|safe, type=type, with_tick=with_tick, delete_button=delete_button, subhead=subhead) }} {{ banner(caller()|safe, type=type, with_tick=with_tick, delete_button=delete_button, subhead=subhead, action=action) }}
{% endmacro %} {% endmacro %}

View File

@@ -2,12 +2,21 @@
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
{% from "components/banner.html" import banner %}
{% block service_page_title %} {% block service_page_title %}
{{ user.name or user.email_localpart }} {{ user.name or user.email_localpart }}
{% endblock %} {% endblock %}
{% block maincolumn_content %} {% block maincolumn_content %}
{% if delete %}
{{ banner(
'Are you sure you want to remove {}?'.format(user.name),
type='dangerous',
delete_button='Yes, remove',
action=url_for('.remove_user_from_service', service_id=current_service.id, user_id=user.id)
) }}
{% endif %}
<h1 class="heading-large"> <h1 class="heading-large">
{{ user.name or user.email_localpart }} {{ user.name or user.email_localpart }}
@@ -28,7 +37,7 @@
{{ page_footer( {{ page_footer(
'Save', 'Save',
delete_link=url_for('.remove_user_from_service', service_id=current_service.id, user_id=user.id) if user or None, delete_link=url_for('.edit_user_permissions', service_id=current_service.id, user_id=user.id, delete='yes'),
delete_link_text='Remove user from service' delete_link_text='Remove user from service'
) }} ) }}

View File

@@ -879,63 +879,45 @@ def test_no_permission_manage_users_page(
assert "Team members" not in resp_text assert "Team members" not in resp_text
def test_get_remove_user_from_service( def test_remove_user_from_service_redirects(
logged_in_client, client_request,
active_user_with_permissions, active_user_with_permissions,
mock_get_users_by_service,
service_one, service_one,
mock_get_users_by_service,
mock_get_template_folders,
mocker, mocker,
): ):
response = logged_in_client.get( page = client_request.get(
url_for( 'main.remove_user_from_service',
'main.remove_user_from_service', service_id=service_one['id'],
service_id=service_one['id'], user_id=active_user_with_permissions.id,
user_id=active_user_with_permissions.id)) _follow_redirects=True
assert response.status_code == 200 )
assert "Are you sure you want to remove" in response.get_data(as_text=True) banner = page.find('div', class_='banner-dangerous')
assert "Remove user from service" in response.get_data(as_text=True) assert banner.contents[0].strip() == "Are you sure you want to remove Test User?"
assert banner.form.attrs['action'] == url_for(
'main.confirm_remove_user_from_service',
service_id=service_one['id'],
user_id=active_user_with_permissions.id
)
def test_remove_user_from_service( def test_confirm_remove_user_from_service(
logged_in_client, client_request,
active_user_with_permissions, active_user_with_permissions,
service_one, service_one,
mocker,
mock_get_users_by_service,
mock_get_user,
mock_remove_user_from_service, mock_remove_user_from_service,
): ):
response = logged_in_client.post( client_request.post(
url_for( 'main.confirm_remove_user_from_service',
'main.remove_user_from_service', service_id=service_one['id'],
service_id=service_one['id'], user_id=active_user_with_permissions.id,
user_id=active_user_with_permissions.id)) _expected_redirect=url_for('main.manage_users', service_id=service_one['id'], _external=True)
assert response.status_code == 302 )
assert response.location == url_for( mock_remove_user_from_service.assert_called_once_with(
'main.manage_users', service_id=service_one['id'], _external=True) service_one['id'],
mock_remove_user_from_service.assert_called_once_with(service_one['id'], str(active_user_with_permissions.id)
str(active_user_with_permissions.id)) )
def test_can_remove_user_from_service_as_platform_admin(
logged_in_client,
service_one,
platform_admin_user,
active_user_with_permissions,
mock_get_users_by_service,
mock_remove_user_from_service,
mocker,
):
response = logged_in_client.post(
url_for(
'main.remove_user_from_service',
service_id=service_one['id'],
user_id=active_user_with_permissions.id))
assert response.status_code == 302
assert response.location == url_for(
'main.manage_users', service_id=service_one['id'], _external=True)
mock_remove_user_from_service.assert_called_once_with(service_one['id'],
str(active_user_with_permissions.id))
def test_can_invite_user_as_platform_admin( def test_can_invite_user_as_platform_admin(