From 17b99c9bf23f735841f957a9f41efb3d7adedcc5 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Fri, 19 Feb 2016 15:02:13 +0000 Subject: [PATCH] Add pages to invite, edit, and delete users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This takes the original prototype version of this page, and, using the same fake data (ie nothing is wired up): - adds an invite users page - adds an edit (and delete) user page Both these pages allow the user to set another user’s permissions. This commit adds images for the ticks and crosses, so we have control over their appearance. --- app/assets/images/cross.png | Bin 0 -> 971 bytes app/assets/images/tick-white.png | Bin 0 -> 622 bytes app/assets/images/tick.png | Bin 0 -> 834 bytes app/assets/images/tick.psd | Bin 0 -> 60625 bytes app/assets/stylesheets/app.scss | 13 ++ app/assets/stylesheets/components/banner.scss | 16 +- app/assets/stylesheets/components/table.scss | 24 +++ app/assets/stylesheets/components/yes-no.scss | 36 +++++ app/assets/stylesheets/main.scss | 1 + app/main/__init__.py | 2 +- app/main/dao/services_dao.py | 8 +- app/main/forms.py | 8 +- app/main/views/index.py | 60 -------- app/main/views/manage_users.py | 141 ++++++++++++++++++ app/templates/components/table.html | 12 ++ app/templates/components/yes-no.html | 17 +++ app/templates/flash_messages.html | 2 +- app/templates/main_nav.html | 2 +- app/templates/views/api-keys.html | 2 +- app/templates/views/invite-user.html | 48 ++++++ app/templates/views/manage-users.html | 95 ++++++------ gulpfile.babel.js | 1 + tests/app/main/views/test_manage_users.py | 87 +++++++++++ 23 files changed, 443 insertions(+), 132 deletions(-) create mode 100644 app/assets/images/cross.png create mode 100644 app/assets/images/tick-white.png create mode 100644 app/assets/images/tick.png create mode 100644 app/assets/images/tick.psd create mode 100644 app/assets/stylesheets/components/yes-no.scss create mode 100644 app/main/views/manage_users.py create mode 100644 app/templates/components/yes-no.html create mode 100644 app/templates/views/invite-user.html create mode 100644 tests/app/main/views/test_manage_users.py diff --git a/app/assets/images/cross.png b/app/assets/images/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..fab3b895c6253a371bb79c98e4a0d74de6faee07 GIT binary patch literal 971 zcmV;+12p`JP)C0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUyCs0gOMfvOOzdA?1IY+l5G4a&a@zd7v z)YrZ@M7}md#!gzrOOUfKSR&0000DbW%=J|NsC0|NsC0|NsC006pj;h5!Hq zAxT6*RCwC#+sksRKn#T8PTP(z@cz$xT&YwF!)VmfAkK!YvPkiNi`Zr)gZV%I-~^n2 z6L11ffd41o&!iI1>mGpb%k-5J;A^^E#{uvDloGuAlOTZ7&j^0{f&fxKF8J;H0%-lH z;IHorAogQ|Mc)(ju6kYeBZ7~Q#^s4EERW17X4aKGxs76NUwY{q1K#t$^@94J!qNaz9WfAhbK8DgmM16V?a_ z{jRt|Kt#?3`wNKJ$!Ko@5j`93DT{id(#7X80kvz-RRW5a7RSGjX#b&JS^Tr; z`D4283J9OS^w~P9`toeOtS<^s*B7Vzg?&kY%D%K;DD4XZ)b@o9M{(a3pt|qwSjzj3 z0QG%m%X2`#E5H%`?w;w8en)^~`khVJL48wzqx$Bq?XbQfz;S(J+jn5UDZr8a=DzXJ zenWu4{^Kzyz*wJ1Zn})6Rc8M)D8OicRIvR0nLPi71y2KTyQKPI!R2QC4}=8E^M6S2 z(O}tuAlw&`+zK9cH_C~Z{7(-0kiwYb$|O=APJb=_HFu&PeVb#?5=IwZ`}{L z0%kW|jehfv$PqBRXKMA^_k>*mvs<2K-?%I82$1SNs@%#Dh}bpB4WIn0d&BN?(|kzXi-a=0l;c>>6PQX6_1^_-|%}Fz(lac@c002ovPDHLkV1ljd@W=oF literal 0 HcmV?d00001 diff --git a/app/assets/images/tick-white.png b/app/assets/images/tick-white.png new file mode 100644 index 0000000000000000000000000000000000000000..c08e57c4f00bfaacc07e911c0ddd5ccbee50d7ae GIT binary patch literal 622 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&NtU=qlmzFem6RtIr7}3C}&gSx? zoGs}Naj!=CNQkNa#aKjYa{e$Hoe`DxC)>Cfk)+8+N|{=dFYuKTy- zfBmJ${a=2l+y3`BUw`Q}|I4raUw(O8UNKz%e~Pp0=YP&X=KFdu6D0Z`BwGB}5y-r5 z4`#Z8m>>nke?39W{a~gqhzVBvAEfp>SnYq1+VAyOoMrQWyPIAq+&+J+e(sC?kvXe! z-^Hn&Vf9$)+)O$Zv&*sZAzyE4l%kTJ}OXH+?EVH91(cy>^ z3~1B;)4eE+b;<+o@5dHsGtGL={r=gC)l9RVai=#bv^S*vJQS{SQgwmnzL<}vi(dKt zVz?&Kcv5$P<~|+6B~~mZho>uYg-vfz`>{*r`08MXzcsNoLV?>GcKzH{qO+8f@$08j z#iJLeG+6zNvYU5xNyD!nQ8r4~w>0ef5w&aCf|iD=A5*PWbpPB^oyJ!8QLK0V{`;o4 jnW7W8pRn4oNH84PqHX>@evdRTB`|op`njxgN@xNA5eOhG literal 0 HcmV?d00001 diff --git a/app/assets/images/tick.png b/app/assets/images/tick.png new file mode 100644 index 0000000000000000000000000000000000000000..77591a5951c87114f2f1e16e4916aa9621589af5 GIT binary patch literal 834 zcmV-I1HJr-P)C0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUyDo{*RMe6nME}330nO_x*OVQ}uCY4yy z>D)u3ZbYMRE0i6rCr7}gKj#GK)z1mgt)CH~Uq3BC$9__Pp8b>nUHb_E`u6t%bnfp2 z=-uB6(7nGApnrcYfJ1*JfJc8RfJ=WNfKUG|fK&e^fLH%1fLs3|fM5SEfMfqAfM@?I zfNOuA&+zRBz`0NT3~2gHfT~Xf==wZ>vQGnO`z(OEPXg%s9Dt!u0a*H3!08)Z`Z>Vq zI3LiXp8;I@bECl6PXliKHi5OD1U&jJ0&_nFc=hW7dp`kK^lJh_e-D_Bt6wPz82ue! z(=Q1q{VibE&kH#H4It6a2}u1lAlXj~Sp5|s(@zO#{UspV-wSyC1)$L13W)tTpx9pv znEe-^(q9Uw{U@N>e+#(%2cV(<6p;IOKvVxNVE1o;#{N}6?_UAU{dxZRVfQZpL0|mT z`w`Clh_i1$0>QO^`3!tY(AK~H6?{ezS`Sw7GlJIs{hN?42<=DP_z6LK|M}mLD-iTw z?*mMMr2l>+;t53k#XBKOAnPyRia7#de|0Wk2&Db>$;d7c_cv!lt3cl0o{nt-MSpic zU=e8g`x_FyK-Ev&5orawe(IJ?Cs6j2_k?n_h%0(1E%$_0VC{S?K5 zz+L}}Qb8c^zX^Wy_mAI~EwM~TdSAFjGoAS>pwammfB_hQ0r(eS0Bl|s!QRnW-2eap M07*qoM6N<$g4)oE3;+NC literal 0 HcmV?d00001 diff --git a/app/assets/images/tick.psd b/app/assets/images/tick.psd new file mode 100644 index 0000000000000000000000000000000000000000..7f90c3098b56ceaee937fcce9e83b505636d060a GIT binary patch literal 60625 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 literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/app.scss b/app/assets/stylesheets/app.scss index e4a629217..57b5db45a 100644 --- a/app/assets/stylesheets/app.scss +++ b/app/assets/stylesheets/app.scss @@ -66,3 +66,16 @@ a { font-family: monospace; overflow-x: scroll; } + +.inline { + + .block-label { + + @include media(tablet) { + float: none; + display: inline-block; + } + + } + +} diff --git a/app/assets/stylesheets/components/banner.scss b/app/assets/stylesheets/components/banner.scss index 3a8898c83..7c2fd2b99 100644 --- a/app/assets/stylesheets/components/banner.scss +++ b/app/assets/stylesheets/components/banner.scss @@ -16,19 +16,13 @@ .banner-with-tick, .banner-default-with-tick { - @extend %banner; padding: $gutter-half ($gutter + $gutter-half); - - &:before { - @include core-24; - content: '✔'; - position: absolute; - top: $gutter-half; - left: $gutter-half; - margin-top: -2px; - } - + background-image: file-url('tick-white.png'); + background-size: 19px; + background-repeat: no-repeat; + background-position: $gutter-half $gutter-half; + font-weight: bold; } .banner-dangerous { diff --git a/app/assets/stylesheets/components/table.scss b/app/assets/stylesheets/components/table.scss index 1b40c5fa8..477ddb208 100644 --- a/app/assets/stylesheets/components/table.scss +++ b/app/assets/stylesheets/components/table.scss @@ -7,6 +7,12 @@ margin: 40px 0 5px 0; } +.table-field-headings { + th { + padding: 0 0 5px 0; + } +} + %table-field, .table-field { @@ -36,6 +42,23 @@ } + &-yes, + &-no { + display: block; + text-indent: -999em; + background-size: 19px 19px; + background-repeat: no-repeat; + background-position: 50% 50%; + } + + &-yes { + background-image: file-url('tick.png'); + } + + &-no { + background-image: file-url('cross.png'); + } + &-missing { color: $error-colour; font-weight: bold; @@ -77,4 +100,5 @@ margin-top: -20px; border-bottom: 1px solid $border-colour; padding-bottom: 10px; + text-align: center; } diff --git a/app/assets/stylesheets/components/yes-no.scss b/app/assets/stylesheets/components/yes-no.scss new file mode 100644 index 000000000..9a71113a5 --- /dev/null +++ b/app/assets/stylesheets/components/yes-no.scss @@ -0,0 +1,36 @@ +.yes-no-wrapper { + border-bottom: 1px solid $border-colour; + margin: 0 0 $gutter 0; +} + +.yes-no { + + border-top: 1px solid $border-colour; + padding: 10px 0; + + &-label { + padding-top: 19px; + float: left; + } + + &-fields { + + text-align: right; + + .block-label { + + @include media(tablet) { + + margin-bottom: 0; + + &:last-child { + margin-right: 0; + } + + } + + } + + } + +} diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index eb420be44..e9c36ad7a 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -47,6 +47,7 @@ $path: '/static/images/'; @import 'components/browse-list'; @import 'components/email-message'; @import 'components/api-key'; +@import 'components/yes-no'; @import 'views/job'; @import 'views/edit-template'; diff --git a/app/main/__init__.py b/app/main/__init__.py index 7ce30ff8a..fbfd2562a 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -5,5 +5,5 @@ main = Blueprint('main', __name__) from app.main.views import ( index, sign_in, sign_out, register, two_factor, verify, sms, add_service, code_not_received, jobs, dashboard, templates, service_settings, forgot_password, - new_password, styleguide, user_profile, choose_service, api_keys + new_password, styleguide, user_profile, choose_service, api_keys, manage_users ) diff --git a/app/main/dao/services_dao.py b/app/main/dao/services_dao.py index 604b2e27d..77f6610dc 100644 --- a/app/main/dao/services_dao.py +++ b/app/main/dao/services_dao.py @@ -1,7 +1,7 @@ -from flask import url_for +from flask import url_for, abort from app import notifications_api_client -from notifications_python_client.errors import HTTPError from app.utils import BrowsableItem +from notifications_python_client.errors import HTTPError def insert_new_service(service_name, user_id): @@ -29,7 +29,9 @@ def get_service_by_id(id_): def get_service_by_id_or_404(id_): try: - return get_service_by_id(id_) + return notifications_api_client.get_service(id_)['data'] + except KeyError: + abort(404) except HTTPError as e: if e.status_code == 404: abort(404) diff --git a/app/main/forms.py b/app/main/forms.py index ddab3fe01..20b007514 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -21,10 +21,10 @@ from app.utils import ( ) -def email_address(): +def email_address(label='Email address'): gov_uk_email \ = "(^[^@^\\s]+@[^@^\\.^\\s]+(\\.[^@^\\.^\\s]*)*.gov.uk)" - return EmailField('Email address', validators=[ + return EmailField(label, validators=[ Length(min=5, max=255), DataRequired(message='Email cannot be empty'), Email(message='Enter a valid email address'), @@ -96,6 +96,10 @@ class RegisterUserForm(Form): password = password() +class InviteUserForm(Form): + email_address = email_address('Their email address') + + class TwoFactorForm(Form): def __init__(self, validate_code_func, *args, **kwargs): ''' diff --git a/app/main/views/index.py b/app/main/views/index.py index 151ee4e80..1200cf65b 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -34,63 +34,3 @@ def send_email(service_id): @login_required def check_email(service_id): return render_template('views/check-email.html') - - -@main.route("/services//manage-users") -@login_required -def manage_users(service_id): - users = [ - { - 'name': 'Henry Hadlow', - 'permission_send_messages': True, - 'permission_manage_service': False, - 'permission_manage_api_keys': False - }, - - { - 'name': 'Pete Herlihy', - 'permission_send_messages': False, - 'permission_manage_service': False, - 'permission_manage_api_keys': False, - }, - { - 'name': 'Chris Hill-Scott', - 'permission_send_messages': True, - 'permission_manage_service': True, - 'permission_manage_api_keys': True - }, - { - 'name': 'Martyn Inglis', - 'permission_send_messages': True, - 'permission_manage_service': True, - 'permission_manage_api_keys': True - } - ] - invited_users = [ - { - 'email_localpart': 'caley.smolska', - 'permission_send_messages': True, - 'permission_manage_service': False, - 'permission_manage_api_keys': False - }, - - { - 'email_localpart': 'ash.stephens', - 'permission_send_messages': False, - 'permission_manage_service': False, - 'permission_manage_api_keys': False - }, - { - 'email_localpart': 'nicholas.staples', - 'permission_send_messages': True, - 'permission_manage_service': True, - 'permission_manage_api_keys': True - }, - { - 'email_localpart': 'adam.shimali', - 'permission_send_messages': True, - 'permission_manage_service': True, - 'permission_manage_api_keys': True - } - ] - return render_template('views/manage-users.html', service_id=service_id, users=users, invited_users=invited_users) diff --git a/app/main/views/manage_users.py b/app/main/views/manage_users.py new file mode 100644 index 000000000..54be424d2 --- /dev/null +++ b/app/main/views/manage_users.py @@ -0,0 +1,141 @@ +from flask import ( + request, + render_template, + redirect, + abort, + url_for, + flash +) + +from flask_login import login_required, current_user + +from app.main import main +from app.main.dao import users_dao +from app.main.forms import InviteUserForm +from app.main.dao.services_dao import get_service_by_id_or_404 +from app import user_api_client + + +fake_users = [ + { + 'name': 'Henry Hadlow', + 'email_localpart': 'henry.hadlow', + 'permission_send_messages': True, + 'permission_manage_service': False, + 'permission_manage_api_keys': False, + 'active': True + }, + + { + 'name': 'Pete Herlihy', + 'email_localpart': 'pete.herlihy', + 'permission_send_messages': False, + 'permission_manage_service': False, + 'permission_manage_api_keys': False, + 'active': True + }, + { + 'name': 'Chris Hill-Scott', + 'email_localpart': 'chris.hill-scott', + 'permission_send_messages': True, + 'permission_manage_service': True, + 'permission_manage_api_keys': True, + 'active': True + }, + { + 'name': 'Martyn Inglis', + 'email_localpart': 'martyn.inglis', + 'permission_send_messages': True, + 'permission_manage_service': True, + 'permission_manage_api_keys': True, + 'active': True + }, + { + 'email_localpart': 'caley.smolska', + 'permission_send_messages': True, + 'permission_manage_service': False, + 'permission_manage_api_keys': False, + 'active': False + }, + + { + 'email_localpart': 'ash.stephens', + 'permission_send_messages': False, + 'permission_manage_service': False, + 'permission_manage_api_keys': False, + 'active': False + } +] + + +@main.route("/services//users") +@login_required +def manage_users(service_id): + return render_template( + 'views/manage-users.html', + service_id=service_id, + users=[ + dict(id=user_id, **user) for (user_id, user) in enumerate(fake_users) if user['active'] + ], + invited_users=[ + dict(id=user_id, **user) for (user_id, user) in enumerate(fake_users) if not user['active'] + ] + ) + + +@main.route("/services//users/invite", methods=['GET', 'POST']) +@login_required +def invite_user(service_id): + + form = InviteUserForm() + + if form.validate_on_submit(): + flash('Invite sent to {}'.format(form.email_address.data), 'default_with_tick') + return redirect(url_for('.manage_users', service_id=service_id)) + + return render_template( + 'views/invite-user.html', + user={}, + service=get_service_by_id_or_404(service_id), + service_id=service_id, + form=form + ) + + +@main.route("/services//users/", methods=['GET', 'POST']) +@login_required +def edit_user(service_id, user_id): + + if request.method == 'POST': + return redirect(url_for('.manage_users', service_id=service_id)) + + return render_template( + 'views/invite-user.html', + user=fake_users[int(user_id)], + user_id=user_id, + service=get_service_by_id_or_404(service_id), + service_id=service_id + ) + + +@main.route("/services//users//delete", methods=['GET', 'POST']) +@login_required +def delete_user(service_id, user_id): + + if request.method == 'POST': + return redirect(url_for('.manage_users', service_id=service_id)) + + user = fake_users[int(user_id)] + + flash( + 'Are you sure you want to delete {}’s account?'.format(user.get('name') or user['email_localpart']), + 'delete' + ) + + return render_template( + 'views/invite-user.html', + user=user, + user_id=user_id, + service=get_service_by_id_or_404(service_id), + service_id=service_id + ) diff --git a/app/templates/components/table.html b/app/templates/components/table.html index ed9b35902..9ab70a29f 100644 --- a/app/templates/components/table.html +++ b/app/templates/components/table.html @@ -55,6 +55,18 @@ {%- endmacro %} +{% macro text_field(text) -%} + {% call field() %} + {{ text }} + {% endcall %} +{%- endmacro %} + +{% macro boolean_field(yes) -%} + {% call field(status='yes' if yes else 'no') %} + {{ "Yes" if yes else "No" }} + {% endcall %} +{%- endmacro %} + {% macro right_aligned_field_heading(text) %} {{ text }} {%- endmacro %} diff --git a/app/templates/components/yes-no.html b/app/templates/components/yes-no.html new file mode 100644 index 000000000..4432d6a9f --- /dev/null +++ b/app/templates/components/yes-no.html @@ -0,0 +1,17 @@ +{% macro yes_no(name, label, current_value=None) %} +
+ + {{ label }} + +
+ + +
+
+{% endmacro %} diff --git a/app/templates/flash_messages.html b/app/templates/flash_messages.html index ec2b1a5d0..bf109421e 100644 --- a/app/templates/flash_messages.html +++ b/app/templates/flash_messages.html @@ -5,7 +5,7 @@ {{ banner( message, 'default' if ((category == 'default') or (category == 'default_with_tick')) else 'dangerous', - delete_button="Yes, delete this template" if 'delete' == category else None, + delete_button="Yes, delete" if 'delete' == category else None, with_tick=True if category == 'default_with_tick' else False )}} {% endfor %} diff --git a/app/templates/main_nav.html b/app/templates/main_nav.html index c4e48051a..7e5563890 100644 --- a/app/templates/main_nav.html +++ b/app/templates/main_nav.html @@ -8,7 +8,7 @@
    diff --git a/app/templates/views/api-keys.html b/app/templates/views/api-keys.html index 7ae0c1b90..b7434514c 100644 --- a/app/templates/views/api-keys.html +++ b/app/templates/views/api-keys.html @@ -57,7 +57,7 @@ {% endif %} {% endcall %} -

    +

    diff --git a/app/templates/views/invite-user.html b/app/templates/views/invite-user.html new file mode 100644 index 000000000..2ca19bc2f --- /dev/null +++ b/app/templates/views/invite-user.html @@ -0,0 +1,48 @@ +{% extends "withnav_template.html" %} +{% from "components/yes-no.html" import yes_no %} +{% from "components/textbox.html" import textbox %} +{% from "components/page-footer.html" import page_footer %} + +{% block page_title %} +Manage users – GOV.UK Notify +{% endblock %} + +{% block maincolumn_content %} + +

    + {{ user.name or user.email_localpart or "Add a new team member" }} +

    + +
    +
    + + {% if user %} +

    + {{ user.email_localpart }}@digital.cabinet-office.gov.uk +

    + {% else %} + {{ textbox(form.email_address, hint='Email address must end in .gov.uk', width='1-1') }} + {% endif %} + +
    + + Permissions + + {{ yes_no('send_messages', 'Send messages', user.permission_send_messages) }} + {{ yes_no('manage_service', 'Manage service', user.permission_manage_service) }} + {{ yes_no('manage_api_keys', 'Manage API keys', user.permission_manage_api_keys) }} +
    + + {% if user %} + {{ page_footer( + 'Save', + delete_link=url_for('.delete_user', service_id=service_id, user_id=user_id), + delete_link_text='delete this account' + ) }} + {% else %} + {{ page_footer('Send invitation email') }} + {% endif %} + +
    +
    +{% endblock %} diff --git a/app/templates/views/manage-users.html b/app/templates/views/manage-users.html index ac881a790..4af05c7aa 100644 --- a/app/templates/views/manage-users.html +++ b/app/templates/views/manage-users.html @@ -1,65 +1,56 @@ {% extends "withnav_template.html" %} -{% from "components/table.html" import list_table, row, field %} +{% from "components/table.html" import list_table, row, field, boolean_field, hidden_field_heading %} {% from "components/page-footer.html" import page_footer %} +{% set table_options = { + 'field_headings': [ + 'Name', 'Send messages', 'Manage service', 'Manage API keys', hidden_field_heading('Link to change') + ], + 'field_headings_visible': True, + 'caption_visible': True +} %} + {% block page_title %} Manage users – GOV.UK Notify {% endblock %} {% block maincolumn_content %} -

    Manage users

    +

    + Manage team +

    -

    - Invite users -

    + {% call(item) list_table( + users, caption='Active', **table_options + ) %} + {% call field() %} + {{ item.name }} + {% endcall %} + {{ boolean_field(item.permission_send_messages) }} + {{ boolean_field(item.permission_manage_service) }} + {{ boolean_field(item.permission_manage_api_keys) }} + {% call field(align='right') %} + Change + {% endcall %} + {% endcall %} + -{% call(item) list_table( - users, - caption='Active users', - field_headings=['Name', 'Send messages', 'Manage Service', 'Manage API keys', 'Link to change'], - field_headings_visible=True, - caption_visible=True -) %} - {% call field() %} - {{ item.name }} - {% endcall %} - {% call field() %} - {{ "✔" if item.permission_send_messages else "❌" }} - {% endcall %} - {% call field() %} - {{ "✔" if item.permission_manage_service else "❌" }} - {% endcall %} - {% call field() %} - {{ "✔" if item.permission_manage_api_keys else "❌" }} - {% endcall %} - {% call field(align='right') %} - Change - {% endcall %} -{% endcall %} - -{% call(item) list_table( - invited_users, - caption='Invited users', - field_headings=['Name', 'Send messages', 'Manage Service', 'Manage API keys', 'Link to change'], - field_headings_visible=True, - caption_visible=True -) %} - {% call field() %} - {{ item.email_localpart }} - {% endcall %} - {% call field() %} - {{ "✔" if item.permission_send_messages else "❌" }} - {% endcall %} - {% call field() %} - {{ "✔" if item.permission_manage_service else "❌" }} - {% endcall %} - {% call field() %} - {{ "✔" if item.permission_manage_api_keys else "❌" }} - {% endcall %} - {% call field(align='right') %} - Change - {% endcall %} -{% endcall %} + {% if invited_users %} + {% call(item) list_table( + invited_users, caption='Invited', **table_options + ) %} + {% call field() %} + {{ item.email_localpart }} + {% endcall %} + {{ boolean_field(item.permission_send_messages) }} + {{ boolean_field(item.permission_manage_service) }} + {{ boolean_field(item.permission_manage_api_keys) }} + {% call field(align='right') %} + Change + {% endcall %} + {% endcall %} + {% endif %} {% endblock %} diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 7fbc03610..3938b00a8 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -82,6 +82,7 @@ gulp.task('watchForChanges', function() { gulp.watch(paths.src + 'javascripts/**/*', ['javascripts']); gulp.watch(paths.src + 'stylesheets/**/*', ['sass']); gulp.watch(paths.src + 'images/**/*', ['images']); + gulp.watch('gulpfile.babel.js', ['default']); }); gulp.task('lint:sass', () => gulp diff --git a/tests/app/main/views/test_manage_users.py b/tests/app/main/views/test_manage_users.py new file mode 100644 index 000000000..000464ecb --- /dev/null +++ b/tests/app/main/views/test_manage_users.py @@ -0,0 +1,87 @@ +import json +from flask import url_for + + +def test_should_show_overview_page( + app_, + api_user_active, + mock_login, + mock_get_service +): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(api_user_active) + response = client.get(url_for('main.manage_users', service_id=55555)) + + assert 'Manage team' in response.get_data(as_text=True) + assert 'Henry Hadlow' in response.get_data(as_text=True) + assert 'caley.smolska' in response.get_data(as_text=True) + assert response.status_code == 200 + + +def test_should_show_page_for_one_user( + app_, + api_user_active, + mock_login, + mock_get_service +): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(api_user_active) + response = client.get(url_for('main.edit_user', service_id=55555, user_id=0)) + + assert 'Henry Hadlow' in response.get_data(as_text=True) + assert response.status_code == 200 + + +def test_redirect_after_saving_user( + app_, + api_user_active, + mock_login, + mock_get_service +): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(api_user_active) + response = client.post(url_for( + 'main.edit_user', service_id=55555, user_id=0 + )) + + assert response.status_code == 302 + assert response.location == url_for( + 'main.manage_users', service_id=55555, _external=True + ) + + +def test_should_show_page_for_inviting_user( + app_, + api_user_active, + mock_login, + mock_get_service +): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(api_user_active) + response = client.get(url_for('main.invite_user', service_id=55555)) + + assert 'Add a new team member' in response.get_data(as_text=True) + assert response.status_code == 200 + + +def test_invite_user( + app_, + api_user_active, + mock_login, + mock_get_service +): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(api_user_active) + response = client.post( + url_for('main.invite_user', service_id=55555), + data={'email_address': 'test@example.gov.uk'}, + follow_redirects=True + ) + + assert response.status_code == 200 + assert 'Invite sent to test@example.gov.uk' in response.get_data(as_text=True)