[WIP] New user can now accept invite and will be made to

register. On succesful register and verfication they
will be added to service and forwarded to dashboard.

Nothing is done yet with the permissions requested in the
invite to the user.
This commit is contained in:
Adam Shimali
2016-03-02 15:25:04 +00:00
parent f9626a7626
commit 6ba13a6513
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)