mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-11 10:28:41 -04:00
Add view that displays user information, including:
- name - email - phone number - services - last login - failed login attempts if any The view can be accessed from results of find_users_by_email logged_in_at added to User serialization on admin frontend as a part of this work
This commit is contained in:
+4
-3
@@ -855,10 +855,11 @@ class SearchTemplatesForm(StripWhitespaceForm):
|
|||||||
|
|
||||||
class SearchUsersByEmailForm(StripWhitespaceForm):
|
class SearchUsersByEmailForm(StripWhitespaceForm):
|
||||||
|
|
||||||
search = SearchField('Search by name or email address',
|
search = SearchField(
|
||||||
|
'Search by name or email address',
|
||||||
validators=[
|
validators=[
|
||||||
DataRequired("You need to enter full or partial e-mail address to search by.")
|
DataRequired("You need to enter full or partial email address to search by.")
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from flask import abort, render_template, request, url_for
|
from flask import render_template, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
from app import user_api_client
|
from app import user_api_client
|
||||||
from app.main import main
|
from app.main import main
|
||||||
from app.utils import user_is_platform_admin
|
|
||||||
from app.main.forms import SearchUsersByEmailForm
|
from app.main.forms import SearchUsersByEmailForm
|
||||||
|
from app.utils import user_is_platform_admin
|
||||||
|
|
||||||
|
|
||||||
@main.route("/find-users-by-email", methods=['GET', 'POST'])
|
@main.route("/find-users-by-email", methods=['GET', 'POST'])
|
||||||
@@ -23,3 +23,14 @@ def find_users_by_email():
|
|||||||
form=form,
|
form=form,
|
||||||
users_found=users_found
|
users_found=users_found
|
||||||
), status
|
), 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
|
||||||
|
)
|
||||||
|
|||||||
+9
-1
@@ -76,15 +76,17 @@ class HeaderNavigation(Navigation):
|
|||||||
'add_organisation',
|
'add_organisation',
|
||||||
'create_email_branding',
|
'create_email_branding',
|
||||||
'email_branding',
|
'email_branding',
|
||||||
|
'find_users_by_email',
|
||||||
'live_services',
|
'live_services',
|
||||||
'organisations',
|
'organisations',
|
||||||
'platform_admin',
|
'platform_admin',
|
||||||
|
'platform_admin_list_complaints',
|
||||||
'suspend_service',
|
'suspend_service',
|
||||||
'trial_services',
|
'trial_services',
|
||||||
'update_email_branding',
|
'update_email_branding',
|
||||||
|
'user_information',
|
||||||
'view_provider',
|
'view_provider',
|
||||||
'view_providers',
|
'view_providers',
|
||||||
'platform_admin_list_complaints',
|
|
||||||
},
|
},
|
||||||
'sign-in': {
|
'sign-in': {
|
||||||
'sign_in',
|
'sign_in',
|
||||||
@@ -399,6 +401,7 @@ class MainNavigation(Navigation):
|
|||||||
'error',
|
'error',
|
||||||
'features',
|
'features',
|
||||||
'feedback',
|
'feedback',
|
||||||
|
'find_users_by_email',
|
||||||
'forgot_password',
|
'forgot_password',
|
||||||
'get_example_csv',
|
'get_example_csv',
|
||||||
'get_notifications_as_json',
|
'get_notifications_as_json',
|
||||||
@@ -472,6 +475,7 @@ class MainNavigation(Navigation):
|
|||||||
'two_factor_email',
|
'two_factor_email',
|
||||||
'two_factor_email_sent',
|
'two_factor_email_sent',
|
||||||
'update_email_branding',
|
'update_email_branding',
|
||||||
|
'user_information',
|
||||||
'user_profile',
|
'user_profile',
|
||||||
'user_profile_email',
|
'user_profile_email',
|
||||||
'user_profile_email_authenticate',
|
'user_profile_email_authenticate',
|
||||||
@@ -567,6 +571,7 @@ class CaseworkNavigation(Navigation):
|
|||||||
'error',
|
'error',
|
||||||
'features',
|
'features',
|
||||||
'feedback',
|
'feedback',
|
||||||
|
'find_users_by_email',
|
||||||
'forgot_password',
|
'forgot_password',
|
||||||
'get_example_csv',
|
'get_example_csv',
|
||||||
'get_notifications_as_json',
|
'get_notifications_as_json',
|
||||||
@@ -687,6 +692,7 @@ class CaseworkNavigation(Navigation):
|
|||||||
'two_factor_email_sent',
|
'two_factor_email_sent',
|
||||||
'update_email_branding',
|
'update_email_branding',
|
||||||
'usage',
|
'usage',
|
||||||
|
'user_information',
|
||||||
'user_profile',
|
'user_profile',
|
||||||
'user_profile_email',
|
'user_profile_email',
|
||||||
'user_profile_email_authenticate',
|
'user_profile_email_authenticate',
|
||||||
@@ -789,6 +795,7 @@ class OrgNavigation(Navigation):
|
|||||||
'error',
|
'error',
|
||||||
'features',
|
'features',
|
||||||
'feedback',
|
'feedback',
|
||||||
|
'find_users_by_email',
|
||||||
'forgot_password',
|
'forgot_password',
|
||||||
'get_example_csv',
|
'get_example_csv',
|
||||||
'get_notifications_as_json',
|
'get_notifications_as_json',
|
||||||
@@ -908,6 +915,7 @@ class OrgNavigation(Navigation):
|
|||||||
'two_factor_email_sent',
|
'two_factor_email_sent',
|
||||||
'update_email_branding',
|
'update_email_branding',
|
||||||
'usage',
|
'usage',
|
||||||
|
'user_information',
|
||||||
'user_profile',
|
'user_profile',
|
||||||
'user_profile_email',
|
'user_profile_email',
|
||||||
'user_profile_email_authenticate',
|
'user_profile_email_authenticate',
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class User(UserMixin):
|
|||||||
self.failed_login_count = fields.get('failed_login_count')
|
self.failed_login_count = fields.get('failed_login_count')
|
||||||
self.state = fields.get('state')
|
self.state = fields.get('state')
|
||||||
self.max_failed_login_count = max_failed_login_count
|
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.platform_admin = fields.get('platform_admin')
|
||||||
self.current_session_id = fields.get('current_session_id')
|
self.current_session_id = fields.get('current_session_id')
|
||||||
self.services = fields.get('services', [])
|
self.services = fields.get('services', [])
|
||||||
|
|||||||
@@ -2,17 +2,13 @@
|
|||||||
{% from "components/page-footer.html" import page_footer %}
|
{% from "components/page-footer.html" import page_footer %}
|
||||||
|
|
||||||
{% block per_page_title %}
|
{% block per_page_title %}
|
||||||
Find users by e-mail
|
Find users by email
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block org_page_title %}
|
|
||||||
Find users by e-mail
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block platform_admin_content %}
|
{% block platform_admin_content %}
|
||||||
|
|
||||||
<h1 class="heading-large">
|
<h1 class="heading-large">
|
||||||
Find users by e-mail
|
Find users by email
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
@@ -24,7 +20,7 @@
|
|||||||
{{ textbox(
|
{{ textbox(
|
||||||
form.search,
|
form.search,
|
||||||
width='1-1',
|
width='1-1',
|
||||||
label='Find users by e-mail, or by partial e-mail'
|
label='Find users by email, or by partial email'
|
||||||
) }}
|
) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="column-one-quarter align-button-with-textbox">
|
<div class="column-one-quarter align-button-with-textbox">
|
||||||
@@ -42,8 +38,8 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{% for user in users_found %}
|
{% for user in users_found %}
|
||||||
<li class="browse-list-item">
|
<li class="browse-list-item">
|
||||||
<a href="#" class="browse-list-link">{{ user.email_address }}</a>
|
<a href="{{url_for('.user_information', user_id=user.id)}}" class="browse-list-link">{{ user.email_address }}</a>
|
||||||
<p class="browse-list-sub-item">{{ user.name }}</p>
|
<p class="browse-list-hint">{{ user.name }}</p>
|
||||||
</li>
|
</li>
|
||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import pytest
|
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from lxml import html
|
from lxml import html
|
||||||
from app.main.views.find_users import find_users_by_email
|
|
||||||
|
|
||||||
|
from app.notify_client.user_api_client import User
|
||||||
from tests import user_json
|
from tests import user_json
|
||||||
from tests.conftest import mock_get_user
|
from tests.conftest import mock_get_user
|
||||||
|
|
||||||
|
|
||||||
def test_find_users_by_email_page_loads_correctly(
|
def test_find_users_by_email_page_loads_correctly(
|
||||||
client,
|
client,
|
||||||
platform_admin_user,
|
platform_admin_user,
|
||||||
mocker
|
mocker
|
||||||
):
|
):
|
||||||
mock_get_user(mocker, user=platform_admin_user)
|
mock_get_user(mocker, user=platform_admin_user)
|
||||||
client.login(platform_admin_user)
|
client.login(platform_admin_user)
|
||||||
@@ -17,29 +17,54 @@ def test_find_users_by_email_page_loads_correctly(
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
document = html.fromstring(response.get_data(as_text=True))
|
document = html.fromstring(response.get_data(as_text=True))
|
||||||
header = document.xpath('//h1')[0].text
|
assert document.xpath("//h1/text()[normalize-space()='Find users by email']")
|
||||||
assert "Find users by e-mail" in header
|
|
||||||
assert len(document.xpath("//input[@type='search']")) > 0
|
assert len(document.xpath("//input[@type='search']")) > 0
|
||||||
|
|
||||||
|
|
||||||
def test_find_users_by_email_displays_users_found(
|
def test_find_users_by_email_displays_users_found(
|
||||||
client,
|
client,
|
||||||
platform_admin_user,
|
platform_admin_user,
|
||||||
mocker
|
mocker
|
||||||
):
|
):
|
||||||
mock_get_user(mocker, user=platform_admin_user)
|
mock_get_user(mocker, user=platform_admin_user)
|
||||||
client.login(platform_admin_user)
|
client.login(platform_admin_user)
|
||||||
mocker.patch('app.user_api_client.find_users_by_full_or_partial_email', return_value={"data": [user_json()]}, autospec=True)
|
mocker.patch(
|
||||||
|
'app.user_api_client.find_users_by_full_or_partial_email',
|
||||||
|
return_value={"data": [user_json()]},
|
||||||
|
autospec=True,
|
||||||
|
)
|
||||||
response = client.post(url_for('main.find_users_by_email'), data={"search": "twilight.sparkle"})
|
response = client.post(url_for('main.find_users_by_email'), data={"search": "twilight.sparkle"})
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
document = html.fromstring(response.get_data(as_text=True))
|
document = html.fromstring(response.get_data(as_text=True))
|
||||||
assert "Test User" in document.text_content()
|
assert document.xpath("//a/text()[normalize-space()='test@gov.uk']")
|
||||||
|
assert document.xpath("//p/text()[normalize-space()='Test User']")
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_users_by_email_displays_multiple_users(
|
||||||
|
client,
|
||||||
|
platform_admin_user,
|
||||||
|
mocker
|
||||||
|
):
|
||||||
|
mock_get_user(mocker, user=platform_admin_user)
|
||||||
|
client.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)
|
||||||
|
response = client.post(url_for('main.find_users_by_email'), data={"search": "apple"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
document = html.fromstring(response.get_data(as_text=True))
|
||||||
|
|
||||||
|
assert document.xpath("//p/text()[normalize-space()='Apple Jack']")
|
||||||
|
assert document.xpath("//p/text()[normalize-space()='Apple Bloom']")
|
||||||
|
|
||||||
|
|
||||||
def test_find_users_by_email_displays_message_if_no_users_found(
|
def test_find_users_by_email_displays_message_if_no_users_found(
|
||||||
client,
|
client,
|
||||||
platform_admin_user,
|
platform_admin_user,
|
||||||
mocker
|
mocker
|
||||||
):
|
):
|
||||||
mock_get_user(mocker, user=platform_admin_user)
|
mock_get_user(mocker, user=platform_admin_user)
|
||||||
client.login(platform_admin_user)
|
client.login(platform_admin_user)
|
||||||
@@ -48,13 +73,13 @@ def test_find_users_by_email_displays_message_if_no_users_found(
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
document = html.fromstring(response.get_data(as_text=True))
|
document = html.fromstring(response.get_data(as_text=True))
|
||||||
assert "No users found." in document.text_content()
|
assert document.xpath("//p/text()[normalize-space()='No users found.']")
|
||||||
|
|
||||||
|
|
||||||
def test_find_users_by_email_validates_against_empty_search_submission(
|
def test_find_users_by_email_validates_against_empty_search_submission(
|
||||||
client,
|
client,
|
||||||
platform_admin_user,
|
platform_admin_user,
|
||||||
mocker
|
mocker
|
||||||
):
|
):
|
||||||
mock_get_user(mocker, user=platform_admin_user)
|
mock_get_user(mocker, user=platform_admin_user)
|
||||||
client.login(platform_admin_user)
|
client.login(platform_admin_user)
|
||||||
@@ -62,4 +87,54 @@ def test_find_users_by_email_validates_against_empty_search_submission(
|
|||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
document = html.fromstring(response.get_data(as_text=True))
|
document = html.fromstring(response.get_data(as_text=True))
|
||||||
assert "You need to enter full or partial e-mail address to search by." in document.text_content()
|
expected_message = "You need to enter full or partial email address to search by."
|
||||||
|
assert document.xpath(
|
||||||
|
"//span[contains(@class, 'error-message') and normalize-space(text()) = '{}']".format(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