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:
Pea Tyczynska
2018-07-13 16:01:52 +01:00
parent 57e9c1d6e6
commit 4cd465753a
7 changed files with 170 additions and 35 deletions
+4 -3
View File
@@ -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.")
] ],
) )
+13 -2
View File
@@ -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
View File
@@ -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',
+1
View File
@@ -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 %}
+95 -20
View File
@@ -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']")