mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-14 10:58:58 -04:00
Merge pull request #2158 from alphagov/admin_platform_users_search
Admin can find users by email and see information about them
This commit is contained in:
@@ -26,6 +26,7 @@ from app.main.views import ( # noqa
|
||||
invites,
|
||||
feedback,
|
||||
providers,
|
||||
find_users,
|
||||
platform_admin,
|
||||
letter_jobs,
|
||||
email_branding,
|
||||
|
||||
@@ -853,6 +853,16 @@ class SearchTemplatesForm(StripWhitespaceForm):
|
||||
search = SearchField('Search by name')
|
||||
|
||||
|
||||
class SearchUsersByEmailForm(StripWhitespaceForm):
|
||||
|
||||
search = SearchField(
|
||||
'Search by name or email address',
|
||||
validators=[
|
||||
DataRequired("You need to enter full or partial email address to search by.")
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class SearchUsersForm(StripWhitespaceForm):
|
||||
|
||||
search = SearchField('Search by name or email address')
|
||||
|
||||
36
app/main/views/find_users.py
Normal file
36
app/main/views/find_users.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from flask import render_template, request
|
||||
from flask_login import login_required
|
||||
|
||||
from app import user_api_client
|
||||
from app.main import main
|
||||
from app.main.forms import SearchUsersByEmailForm
|
||||
from app.utils import user_is_platform_admin
|
||||
|
||||
|
||||
@main.route("/find-users-by-email", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@user_is_platform_admin
|
||||
def find_users_by_email():
|
||||
form = SearchUsersByEmailForm()
|
||||
users_found = None
|
||||
status = 200
|
||||
if form.validate_on_submit():
|
||||
users_found = user_api_client.find_users_by_full_or_partial_email(form.search.data)['data']
|
||||
elif request.method == 'POST':
|
||||
status = 400
|
||||
return render_template(
|
||||
'views/find-users/find-users-by-email.html',
|
||||
form=form,
|
||||
users_found=users_found
|
||||
), status
|
||||
|
||||
|
||||
@main.route("/users/<user_id>", methods=['GET'])
|
||||
@login_required
|
||||
@user_is_platform_admin
|
||||
def user_information(user_id):
|
||||
user = user_api_client.get_user(user_id)
|
||||
return render_template(
|
||||
'views/find-users/user-information.html',
|
||||
user=user
|
||||
)
|
||||
@@ -76,15 +76,17 @@ class HeaderNavigation(Navigation):
|
||||
'add_organisation',
|
||||
'create_email_branding',
|
||||
'email_branding',
|
||||
'find_users_by_email',
|
||||
'live_services',
|
||||
'organisations',
|
||||
'platform_admin',
|
||||
'platform_admin_list_complaints',
|
||||
'suspend_service',
|
||||
'trial_services',
|
||||
'update_email_branding',
|
||||
'user_information',
|
||||
'view_provider',
|
||||
'view_providers',
|
||||
'platform_admin_list_complaints',
|
||||
},
|
||||
'sign-in': {
|
||||
'sign_in',
|
||||
@@ -399,6 +401,7 @@ class MainNavigation(Navigation):
|
||||
'error',
|
||||
'features',
|
||||
'feedback',
|
||||
'find_users_by_email',
|
||||
'forgot_password',
|
||||
'get_example_csv',
|
||||
'get_notifications_as_json',
|
||||
@@ -472,6 +475,7 @@ class MainNavigation(Navigation):
|
||||
'two_factor_email',
|
||||
'two_factor_email_sent',
|
||||
'update_email_branding',
|
||||
'user_information',
|
||||
'user_profile',
|
||||
'user_profile_email',
|
||||
'user_profile_email_authenticate',
|
||||
@@ -567,6 +571,7 @@ class CaseworkNavigation(Navigation):
|
||||
'error',
|
||||
'features',
|
||||
'feedback',
|
||||
'find_users_by_email',
|
||||
'forgot_password',
|
||||
'get_example_csv',
|
||||
'get_notifications_as_json',
|
||||
@@ -687,6 +692,7 @@ class CaseworkNavigation(Navigation):
|
||||
'two_factor_email_sent',
|
||||
'update_email_branding',
|
||||
'usage',
|
||||
'user_information',
|
||||
'user_profile',
|
||||
'user_profile_email',
|
||||
'user_profile_email_authenticate',
|
||||
@@ -789,6 +795,7 @@ class OrgNavigation(Navigation):
|
||||
'error',
|
||||
'features',
|
||||
'feedback',
|
||||
'find_users_by_email',
|
||||
'forgot_password',
|
||||
'get_example_csv',
|
||||
'get_notifications_as_json',
|
||||
@@ -908,6 +915,7 @@ class OrgNavigation(Navigation):
|
||||
'two_factor_email_sent',
|
||||
'update_email_branding',
|
||||
'usage',
|
||||
'user_information',
|
||||
'user_profile',
|
||||
'user_profile_email',
|
||||
'user_profile_email_authenticate',
|
||||
|
||||
@@ -59,6 +59,7 @@ class User(UserMixin):
|
||||
self.failed_login_count = fields.get('failed_login_count')
|
||||
self.state = fields.get('state')
|
||||
self.max_failed_login_count = max_failed_login_count
|
||||
self.logged_in_at = fields.get('logged_in_at')
|
||||
self.platform_admin = fields.get('platform_admin')
|
||||
self.current_session_id = fields.get('current_session_id')
|
||||
self.services = fields.get('services', [])
|
||||
|
||||
@@ -179,6 +179,12 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
data = {'email': email_address}
|
||||
self.post(endpoint, data=data)
|
||||
|
||||
def find_users_by_full_or_partial_email(self, email_address):
|
||||
endpoint = '/user/find-users-by-email'
|
||||
data = {'email': email_address}
|
||||
users = self.post(endpoint, data=data)
|
||||
return users
|
||||
|
||||
def is_email_already_in_use(self, email_address):
|
||||
if self.get_user_by_email_or_none(email_address):
|
||||
return True
|
||||
|
||||
51
app/templates/views/find-users/find-users-by-email.html
Normal file
51
app/templates/views/find-users/find-users-by-email.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends "views/platform-admin/_base_template.html" %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Find users by email
|
||||
{% endblock %}
|
||||
|
||||
{% block platform_admin_content %}
|
||||
|
||||
<h1 class="heading-large">
|
||||
Find users by email
|
||||
</h1>
|
||||
|
||||
<form
|
||||
method="post"
|
||||
action="{{ url_for('.find_users_by_email') }}"
|
||||
class="grid-row"
|
||||
>
|
||||
<div class="column-three-quarters">
|
||||
{{ textbox(
|
||||
form.search,
|
||||
width='1-1',
|
||||
label='Find users by email, or by partial email'
|
||||
) }}
|
||||
</div>
|
||||
<div class="column-one-quarter align-button-with-textbox">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="button">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form id="search-form" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
</form>
|
||||
|
||||
{% if users_found %}
|
||||
<nav class="browse-list">
|
||||
<ul>
|
||||
{% for user in users_found %}
|
||||
<li class="browse-list-item">
|
||||
<a href="{{url_for('.user_information', user_id=user.id)}}" class="browse-list-link">{{ user.email_address }}</a>
|
||||
<p class="browse-list-hint">{{ user.name }}</p>
|
||||
</li>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% elif users_found == [] %}
|
||||
<p class="browse-list-hint">No users found.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
43
app/templates/views/find-users/user-information.html
Normal file
43
app/templates/views/find-users/user-information.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends "views/platform-admin/_base_template.html" %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
|
||||
{% block per_page_title %}
|
||||
User information for {{ user.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block platform_admin_content %}
|
||||
<div class="grid-row bottom-gutter">
|
||||
<div class="column-whole">
|
||||
<h1 class="heading-large">
|
||||
{{ user.name }}
|
||||
</h1>
|
||||
<p>{{ user.email_address }}</p>
|
||||
<p>{{ user.mobile_number }}</p>
|
||||
<h2 class="heading-medium">Services</h2>
|
||||
<nav class="browse-list">
|
||||
<ul>
|
||||
{% for service in user.services %}
|
||||
<li class="browse-list-item">
|
||||
<p class="browse-list-sub-item">{{ service.name }}</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
<h2 class="heading-medium">Last login</h2>
|
||||
{% if not user.logged_in_at %}
|
||||
<p>This person has never logged in</p>
|
||||
{% else %}
|
||||
<p>Last logged in
|
||||
<time class="timeago" datetime="{{ user.logged_in_at }}">
|
||||
{{ user.logged_in_at|format_delta }}
|
||||
</time>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if user.failed_login_count > 0 %}
|
||||
<p style="color:#b10e1e;">
|
||||
{{ user.failed_login_count }} failed login attempts
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -20,6 +20,7 @@
|
||||
('Email branding', url_for('main.email_branding')),
|
||||
('Letter jobs', url_for('main.letter_jobs')),
|
||||
('Inbound SMS numbers', url_for('main.inbound_sms_admin')),
|
||||
('Find users by email', url_for('main.find_users_by_email')),
|
||||
('Email Complaints', url_for('main.platform_admin_list_complaints'))
|
||||
] %}
|
||||
<li>
|
||||
|
||||
133
tests/app/main/views/test_find_users.py
Normal file
133
tests/app/main/views/test_find_users.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from flask import url_for
|
||||
from lxml import html
|
||||
|
||||
from app.notify_client.user_api_client import User
|
||||
from tests import user_json
|
||||
|
||||
|
||||
def test_find_users_by_email_page_loads_correctly(client_request, platform_admin_user):
|
||||
client_request.login(platform_admin_user)
|
||||
document = client_request.get('main.find_users_by_email')
|
||||
|
||||
assert document.h1.text.strip() == 'Find users by email'
|
||||
assert len(document.find_all('input', {'type': 'search'})) > 0
|
||||
|
||||
|
||||
def test_find_users_by_email_displays_users_found(
|
||||
client_request,
|
||||
platform_admin_user,
|
||||
mocker
|
||||
):
|
||||
client_request.login(platform_admin_user)
|
||||
mocker.patch(
|
||||
'app.user_api_client.find_users_by_full_or_partial_email',
|
||||
return_value={"data": [user_json()]},
|
||||
autospec=True,
|
||||
)
|
||||
document = client_request.post(
|
||||
'main.find_users_by_email',
|
||||
_data={"search": "twilight.sparkle"},
|
||||
_expected_status=200
|
||||
)
|
||||
|
||||
assert any(element.text.strip() == 'test@gov.uk' for element in document.find_all(
|
||||
'a', {'class': 'browse-list-link'}, href=True)
|
||||
)
|
||||
assert any(element.text.strip() == 'Test User' for element in document.find_all('p', {'class': 'browse-list-hint'}))
|
||||
|
||||
assert document.find('a', {'class': 'browse-list-link'}).text.strip() == 'test@gov.uk'
|
||||
assert document.find('p', {'class': 'browse-list-hint'}).text.strip() == 'Test User'
|
||||
|
||||
|
||||
def test_find_users_by_email_displays_multiple_users(
|
||||
client_request,
|
||||
platform_admin_user,
|
||||
mocker
|
||||
):
|
||||
client_request.login(platform_admin_user)
|
||||
mocker.patch(
|
||||
'app.user_api_client.find_users_by_full_or_partial_email',
|
||||
return_value={"data": [user_json(name="Apple Jack"), user_json(name="Apple Bloom")]},
|
||||
autospec=True,
|
||||
)
|
||||
document = client_request.post('main.find_users_by_email', _data={"search": "apple"}, _expected_status=200)
|
||||
|
||||
assert any(
|
||||
element.text.strip() == 'Apple Jack' for element in document.find_all('p', {'class': 'browse-list-hint'})
|
||||
)
|
||||
assert any(
|
||||
element.text.strip() == 'Apple Bloom' for element in document.find_all('p', {'class': 'browse-list-hint'})
|
||||
)
|
||||
|
||||
|
||||
def test_find_users_by_email_displays_message_if_no_users_found(
|
||||
client_request,
|
||||
platform_admin_user,
|
||||
mocker
|
||||
):
|
||||
client_request.login(platform_admin_user)
|
||||
mocker.patch('app.user_api_client.find_users_by_full_or_partial_email', return_value={"data": []}, autospec=True)
|
||||
document = client_request.post(
|
||||
'main.find_users_by_email', _data={"search": "twilight.sparkle"}, _expected_status=200
|
||||
)
|
||||
|
||||
assert document.find('p', {'class': 'browse-list-hint'}).text.strip() == 'No users found.'
|
||||
|
||||
|
||||
def test_find_users_by_email_validates_against_empty_search_submission(
|
||||
client_request,
|
||||
platform_admin_user,
|
||||
mocker
|
||||
):
|
||||
client_request.login(platform_admin_user)
|
||||
document = client_request.post('main.find_users_by_email', _data={"search": ""}, _expected_status=400)
|
||||
|
||||
expected_message = "You need to enter full or partial email address to search by."
|
||||
assert document.find('span', {'class': 'error-message'}).text.strip() == expected_message
|
||||
|
||||
|
||||
def test_user_information_page_shows_information_about_user(
|
||||
client,
|
||||
platform_admin_user,
|
||||
mocker
|
||||
):
|
||||
mocker.patch('app.user_api_client.get_user', side_effect=[
|
||||
platform_admin_user,
|
||||
User(user_json(name="Apple Bloom", services=[
|
||||
{"id": 1, "name": "Fresh Orchard Juice"},
|
||||
{"id": 2, "name": "Nature Therapy"},
|
||||
]))
|
||||
], autospec=True)
|
||||
client.login(platform_admin_user)
|
||||
response = client.get(url_for('main.user_information', user_id=345))
|
||||
assert response.status_code == 200
|
||||
|
||||
document = html.fromstring(response.get_data(as_text=True))
|
||||
|
||||
assert document.xpath("//h1/text()[normalize-space()='Apple Bloom']")
|
||||
assert document.xpath("//p/text()[normalize-space()='test@gov.uk']")
|
||||
assert document.xpath("//p/text()[normalize-space()='+447700900986']")
|
||||
|
||||
assert document.xpath("//h2/text()[normalize-space()='Services']")
|
||||
assert document.xpath("//p/text()[normalize-space()='Fresh Orchard Juice']")
|
||||
assert document.xpath("//p/text()[normalize-space()='Nature Therapy']")
|
||||
|
||||
assert document.xpath("//h2/text()[normalize-space()='Last login']")
|
||||
assert not document.xpath("//p/text()[normalize-space()='0 failed login attempts']")
|
||||
|
||||
|
||||
def test_user_information_page_displays_if_there_are_failed_login_attempts(
|
||||
client,
|
||||
platform_admin_user,
|
||||
mocker
|
||||
):
|
||||
mocker.patch('app.user_api_client.get_user', side_effect=[
|
||||
platform_admin_user,
|
||||
User(user_json(name="Apple Bloom", failed_login_count=2))
|
||||
], autospec=True)
|
||||
client.login(platform_admin_user)
|
||||
response = client.get(url_for('main.user_information', user_id=345))
|
||||
assert response.status_code == 200
|
||||
|
||||
document = html.fromstring(response.get_data(as_text=True))
|
||||
assert document.xpath("//p/text()[normalize-space()='2 failed login attempts']")
|
||||
Reference in New Issue
Block a user