Merge pull request #219 from alphagov/accept-invite-part-2

[WIP] New user can now accept invite and will be made to
This commit is contained in:
Rebecca Law
2016-03-02 17:58:00 +00:00
12 changed files with 257 additions and 68 deletions

View File

@@ -8,7 +8,8 @@ from wtforms import (
TextAreaField,
FileField,
RadioField,
BooleanField
BooleanField,
HiddenField
)
from wtforms.fields.html5 import EmailField, TelField
from wtforms.validators import DataRequired, Email, Length, Regexp
@@ -97,6 +98,15 @@ class RegisterUserForm(Form):
password = password()
class RegisterUserFromInviteForm(Form):
name = StringField('Full name',
validators=[DataRequired(message='Name can not be empty')])
mobile_number = mobile_number()
password = password()
service = HiddenField('service')
email_address = HiddenField('email_address')
class InviteUserForm(Form):
email_address = email_address('Their email address')

View File

@@ -1,15 +1,31 @@
import re
from flask import (
render_template,
redirect,
session,
url_for
)
from flask_login import login_required
from flask import render_template, request, redirect, session, url_for
from flask_login import login_required, current_user
from app.main import main
from app.main.dao import services_dao, users_dao
from app.main.forms import AddServiceForm
from app import user_api_client
@main.route("/add-service", methods=['GET', 'POST'])
@login_required
def add_service():
invited_user = session.get('invited_user')
if invited_user:
# if invited user add to service and redirect to dashboard
user = users_dao.get_user_by_id(session['user_id'])
service_id = invited_user['service']
user_api_client.add_user_to_service(service_id, user.id)
session.pop('invited_user', None)
return redirect(url_for('main.service_dashboard', service_id=service_id))
form = AddServiceForm(services_dao.find_all_service_names)
heading = 'Which service do you want to set up notifications for?'
if form.validate_on_submit():

View File

@@ -12,12 +12,6 @@ def index():
return render_template('views/signedout.html')
@main.route("/register-from-invite")
@login_required
def register_from_invite():
return render_template('views/register-from-invite.html')
@main.route("/verify-mobile")
@login_required
def verify_mobile():

View File

@@ -1,7 +1,8 @@
from flask import (
redirect,
url_for,
abort
abort,
session
)
from notifications_python_client.errors import HTTPError
@@ -15,6 +16,7 @@ from app import (
@main.route("/invitation/<token>")
def accept_invite(token):
try:
invited_user = invite_api_client.accept_invite(token)
existing_user = user_api_client.get_user_by_email(invited_user.email_address)
@@ -22,11 +24,11 @@ def accept_invite(token):
if existing_user:
user_api_client.add_user_to_service(invited_user.service,
existing_user.id)
return redirect(url_for('main.service_dashboard', service_id=invited_user.service))
else:
# TODO implement registration flow for new users
abort(404)
session['invited_user'] = invited_user.serialize()
return redirect(url_for('main.register_from_invite'))
except HTTPError as e:
if e.status_code == 404:
abort(404)

View File

@@ -15,7 +15,10 @@ from notifications_python_client.errors import HTTPError
from app.main import main
from app.main.dao import users_dao
from app.main.forms import RegisterUserForm
from app.main.forms import (
RegisterUserForm,
RegisterUserFromInviteForm
)
from app import user_api_client
@@ -27,29 +30,53 @@ def register():
form = RegisterUserForm()
if form.validate_on_submit():
if users_dao.is_email_unique(form.email_address.data):
try:
user = user_api_client.register_user(form.name.data,
form.email_address.data,
form.mobile_number.data,
form.password.data)
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
return _do_registration(form)
else:
return render_template('views/register.html', form=form)
# TODO possibly there should be some exception handling
# for sending sms and email codes.
# How do we report to the user there is a problem with
# sending codes apart from service unavailable?
# at the moment i believe http 500 is fine.
users_dao.send_verify_code(user.id, 'sms', user.mobile_number)
users_dao.send_verify_code(user.id, 'email', user.email_address)
session['expiry_date'] = str(datetime.now() + timedelta(hours=1))
session['user_details'] = {"email": user.email_address, "id": user.id}
return redirect(url_for('main.verify'))
else:
flash('There was an error registering your account')
return render_template('views/register.html', form=form)
@main.route('/register-from-invite', methods=['GET', 'POST'])
def register_from_invite():
form = RegisterUserFromInviteForm()
invited_user = session.get('invited_user')
if not invited_user:
abort(404)
if form.validate_on_submit():
if form.service.data != invited_user['service'] or form.email_address.data != invited_user['email_address']:
abort(400)
return _do_registration(form)
form.service.data = invited_user['service']
form.email_address.data = invited_user['email_address']
return render_template('views/register-from-invite.html', form=form)
def _do_registration(form, service=None):
if users_dao.is_email_unique(form.email_address.data):
try:
user = user_api_client.register_user(form.name.data,
form.email_address.data,
form.mobile_number.data,
form.password.data)
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
# TODO possibly there should be some exception handling
# for sending sms and email codes.
# How do we report to the user there is a problem with
# sending codes apart from service unavailable?
# at the moment i believe http 500 is fine.
users_dao.send_verify_code(user.id, 'sms', user.mobile_number)
users_dao.send_verify_code(user.id, 'email', user.email_address)
session['expiry_date'] = str(datetime.now() + timedelta(hours=1))
session['user_details'] = {"email": user.email_address, "id": user.id}
return redirect(url_for('main.verify'))
else:
flash('There was an error registering your account')

View File

@@ -17,6 +17,7 @@ from app.main.forms import VerifyForm
@main.route('/verify', methods=['GET', 'POST'])
def verify():
# TODO there needs to be a way to regenerate a session id
# or handle gracefully.
user_id = session['user_details']['id']

View File

@@ -125,3 +125,12 @@ class InvitedUser(object):
def has_permissions(self, permission):
return permission in self.permissions
def serialize(self):
return {'id': self.id,
'service': self.service,
'from_user': self.from_user,
'email_address': self.email_address,
'permissions': self.permissions,
'status': self.status
}

View File

@@ -34,8 +34,14 @@ class UserApiClient(BaseAPIClient):
return User(user_data['data'], max_failed_login_count=self.max_failed_login_count)
def get_user_by_email(self, email_address):
params = {'email': email_address}
user_data = self.get('/user/email', params=params)
try:
params = {'email': email_address}
user_data = self.get('/user/email', params=params)
except HTTPError as e:
if e.status_code == 404:
return None
else:
raise e
return User(user_data['data'], max_failed_login_count=self.max_failed_login_count)
def get_users(self):

View File

@@ -1,4 +1,6 @@
{% extends "withoutnav_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block page_title %}
Create an account GOV.UK Notify
@@ -9,26 +11,14 @@ Create an account GOV.UK Notify
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-large">Create an account</h1>
<p>If you've used GOV.UK Notify before, <a href="">sign in to your account</a>.</p>
<p>
<label class="form-label" for="name-f1">Full name</label>
<input class="form-control-2-3" id="name-f1" type="text">
</p>
<p>
<label class="form-label" for="mobile">Mobile phone number</label>
<input class="form-control-1-4" id="mobile" type="text">
</p>
<p>
<label class="form-label" for="password">Create a password</label>
<input class="form-control-1-4" id="password" type="password"><br>
<span class="font-xsmall">Your password must have at least 10 characters</span></label>
</p>
<p>
<a class="button" href="verify-mobile" role="button">Continue</a>
</p>
<form method="post" autocomplete="nope">
{{ textbox(form.name, width='3-4') }}
{{ textbox(form.mobile_number, width='3-4') }}
{{ textbox(form.password, hint="Your password must have at least 10 characters", width='3-4') }}
{{ page_footer("Continue") }}
{{form.service}}
{{form.email_address}}
</form>
</div>
</div>

View File

@@ -1,9 +1,6 @@
from flask import url_for
from app.notify_client.models import InvitedUser
from notifications_python_client.errors import HTTPError
import pytest
from bs4 import BeautifulSoup
def test_existing_user_accept_invite_calls_api_and_redirects_to_dashboard(app_,
@@ -28,3 +25,142 @@ def test_existing_user_accept_invite_calls_api_and_redirects_to_dashboard(app_,
assert response.status_code == 302
assert response.location == expected_redirect_location
def test_existing_signed_out_user_accept_invite_redirects_to_sign_in(app_,
service_one,
api_user_active,
sample_invite,
mock_accept_invite,
mock_get_user_by_email,
mock_add_user_to_service):
expected_service = service_one['id']
expected_redirect_location = 'http://localhost/services/{}/dashboard'.format(expected_service)
with app_.test_request_context():
with app_.test_client() as client:
response = client.get(url_for('main.accept_invite', token='thisisnotarealtoken'), follow_redirects=True)
mock_accept_invite.assert_called_with('thisisnotarealtoken')
mock_get_user_by_email.assert_called_with('invited_user@test.gov.uk')
mock_add_user_to_service.assert_called_with(expected_service, api_user_active.id)
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
assert page.h1.string.strip() == 'Sign in'
def test_new_user_accept_invite_calls_api_and_redirects_to_registration(app_,
service_one,
sample_invite,
mock_accept_invite,
mock_dont_get_user_by_email,
mock_add_user_to_service):
expected_service = service_one['id']
expected_redirect_location = 'http://localhost/register-from-invite'
with app_.test_request_context():
with app_.test_client() as client:
response = client.get(url_for('main.accept_invite', token='thisisnotarealtoken'))
mock_accept_invite.assert_called_with('thisisnotarealtoken')
mock_dont_get_user_by_email.assert_called_with('invited_user@test.gov.uk')
assert response.status_code == 302
assert response.location == expected_redirect_location
def test_new_user_accept_invite_completes_new_registration_redirects_to_verify(app_,
service_one,
sample_invite,
mock_accept_invite,
mock_dont_get_user_by_email,
mock_register_user,
mock_send_verify_code,
mock_add_user_to_service):
expected_service = service_one['id']
expected_email = sample_invite['email_address']
expected_from_user = service_one['users'][0]
expected_redirect_location = 'http://localhost/register-from-invite'
with app_.test_request_context():
with app_.test_client() as client:
response = client.get(url_for('main.accept_invite', token='thisisnotarealtoken'))
with client.session_transaction() as session:
assert response.status_code == 302
assert response.location == expected_redirect_location
invited_user = session.get('invited_user')
assert invited_user
assert expected_service == invited_user['service']
assert expected_email == invited_user['email_address']
assert expected_from_user == invited_user['from_user']
data = {'service': invited_user['service'],
'email_address': invited_user['email_address'],
'from_user': invited_user['from_user'],
'password': 'longpassword',
'mobile_number': '+447890123456',
'name': 'Invited User'
}
expected_redirect_location = 'http://localhost/verify'
response = client.post(url_for('main.register_from_invite'), data=data)
assert response.status_code == 302
assert response.location == expected_redirect_location
mock_register_user.assert_called_with(data['name'],
data['email_address'],
data['mobile_number'],
data['password'])
def test_new_invited_user_verifies_and_added_to_service(app_,
service_one,
sample_invite,
mock_accept_invite,
mock_dont_get_user_by_email,
mock_register_user,
mock_send_verify_code,
mock_check_verify_code,
mock_get_user,
mock_update_user,
mock_add_user_to_service,
mock_get_service,
mock_get_service_templates,
mock_get_jobs):
with app_.test_request_context():
with app_.test_client() as client:
# visit accept token page
response = client.get(url_for('main.accept_invite', token='thisisnotarealtoken'))
data = {'service': sample_invite['service'],
'email_address': sample_invite['email_address'],
'from_user': sample_invite['from_user'],
'password': 'longpassword',
'mobile_number': '+447890123456',
'name': 'Invited User'
}
# get redirected to register from invite
response = client.post(url_for('main.register_from_invite'), data=data)
# that sends user on to verify
response = client.post(url_for('main.verify'), data={'sms_code': '12345', 'email_code': '23456'},
follow_redirects=True)
# when they post codes back to admin user should be added to
# service and sent on to dash board
with client.session_transaction() as session:
new_user_id = session['user_id']
mock_add_user_to_service.assert_called_with(data['service'], new_user_id)
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
element = page.find('h2', class_='navigation-service-name').find('a')
assert element.text == 'Test Service'
service_link = element.attrs['href']
assert service_link == '/services/{}/dashboard'.format(service_one['id'])

View File

@@ -252,7 +252,6 @@ def test_should_redirect_after_mobile_number_confirm(app_,
response = client.post(
url_for('main.user_profile_mobile_number_confirm'),
data=data)
print(response.get_data(as_text=True))
assert response.status_code == 302
assert response.location == url_for(
'main.user_profile', _external=True)
@@ -287,7 +286,6 @@ def test_should_redirect_after_password_change(app_,
url_for('main.user_profile_password'),
data=data)
print(response.get_data(as_text=True))
assert response.status_code == 302
assert response.location == url_for(
'main.user_profile', _external=True)

View File

@@ -572,7 +572,7 @@ def sample_invite(mocker, service_one):
email_address = 'invited_user@test.gov.uk'
service_id = service_one['id']
permissions = 'send_messages,manage_service,manage_api_keys'
created_at = datetime.datetime.now()
created_at = str(datetime.datetime.now())
return invite_json(id, from_user, service_id, email_address, permissions, created_at)