From 4cd465753a870951b8322bf275924d93d964df0e Mon Sep 17 00:00:00 2001 From: Pea Tyczynska Date: Tue, 10 Jul 2018 17:24:20 +0100 Subject: [PATCH] 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 --- app/main/forms.py | 7 +- app/main/views/find_users.py | 15 ++- app/navigation.py | 10 +- app/notify_client/models.py | 1 + .../views/find-users/find-users-by-email.html | 14 +-- .../views/find-users/user-information.html | 43 +++++++ tests/app/main/views/test_find_users.py | 115 +++++++++++++++--- 7 files changed, 170 insertions(+), 35 deletions(-) create mode 100644 app/templates/views/find-users/user-information.html diff --git a/app/main/forms.py b/app/main/forms.py index 2a2e3a5df..f0f7d790a 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -855,10 +855,11 @@ class SearchTemplatesForm(StripWhitespaceForm): class SearchUsersByEmailForm(StripWhitespaceForm): - search = SearchField('Search by name or email address', + search = SearchField( + 'Search by name or email address', 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.") + ], ) diff --git a/app/main/views/find_users.py b/app/main/views/find_users.py index 13c98fb50..23a961afe 100644 --- a/app/main/views/find_users.py +++ b/app/main/views/find_users.py @@ -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 app import user_api_client from app.main import main -from app.utils import user_is_platform_admin from app.main.forms import SearchUsersByEmailForm +from app.utils import user_is_platform_admin @main.route("/find-users-by-email", methods=['GET', 'POST']) @@ -23,3 +23,14 @@ def find_users_by_email(): form=form, users_found=users_found ), status + + +@main.route("/users/", 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 + ) diff --git a/app/navigation.py b/app/navigation.py index 4860e4adf..076af3d12 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -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', diff --git a/app/notify_client/models.py b/app/notify_client/models.py index 3ccdce48b..c3d2564a2 100644 --- a/app/notify_client/models.py +++ b/app/notify_client/models.py @@ -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', []) diff --git a/app/templates/views/find-users/find-users-by-email.html b/app/templates/views/find-users/find-users-by-email.html index 1ff768d0f..7ad9939bb 100644 --- a/app/templates/views/find-users/find-users-by-email.html +++ b/app/templates/views/find-users/find-users-by-email.html @@ -2,17 +2,13 @@ {% from "components/page-footer.html" import page_footer %} {% block per_page_title %} - Find users by e-mail -{% endblock %} - -{% block org_page_title %} - Find users by e-mail + Find users by email {% endblock %} {% block platform_admin_content %}

- Find users by e-mail + Find users by email

@@ -42,8 +38,8 @@
    {% for user in users_found %}
  • - {{ user.email_address }} -

    {{ user.name }}

    + {{ user.email_address }} +

    {{ user.name }}


  • {% endfor %} diff --git a/app/templates/views/find-users/user-information.html b/app/templates/views/find-users/user-information.html new file mode 100644 index 000000000..5c9281615 --- /dev/null +++ b/app/templates/views/find-users/user-information.html @@ -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 %} +
    +
    +

    + {{ user.name }} +

    +

    {{ user.email_address }}

    +

    {{ user.mobile_number }}

    +

    Services

    + +

    Last login

    + {% if not user.logged_in_at %} +

    This person has never logged in

    + {% else %} +

    Last logged in + +

    + {% endif %} + {% if user.failed_login_count > 0 %} +

    + {{ user.failed_login_count }} failed login attempts +

    + {% endif %} +
    +
    +{% endblock %} diff --git a/tests/app/main/views/test_find_users.py b/tests/app/main/views/test_find_users.py index ace7e803c..9d3394776 100644 --- a/tests/app/main/views/test_find_users.py +++ b/tests/app/main/views/test_find_users.py @@ -1,15 +1,15 @@ -import pytest from flask import url_for 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.conftest import mock_get_user + def test_find_users_by_email_page_loads_correctly( - client, - platform_admin_user, - mocker + client, + platform_admin_user, + mocker ): mock_get_user(mocker, user=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 document = html.fromstring(response.get_data(as_text=True)) - header = document.xpath('//h1')[0].text - assert "Find users by e-mail" in header + assert document.xpath("//h1/text()[normalize-space()='Find users by email']") assert len(document.xpath("//input[@type='search']")) > 0 def test_find_users_by_email_displays_users_found( - client, - platform_admin_user, - mocker + 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()]}, 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"}) assert response.status_code == 200 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( - client, - platform_admin_user, - mocker + client, + platform_admin_user, + mocker ): mock_get_user(mocker, user=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 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( - client, - platform_admin_user, - mocker + client, + platform_admin_user, + mocker ): mock_get_user(mocker, user=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 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']")