From 13ef2d7bae4a8d0db17e1c1270fe95ab09b1d696 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Tue, 20 Feb 2018 17:09:16 +0000 Subject: [PATCH] - new endpoint to check the token for an org invitation. - new endpoint to add user to organisation - new endpoint to return users for an organisation --- app/__init__.py | 4 +++ app/dao/organisation_dao.py | 22 ++++++++++++- app/models.py | 14 +++++--- .../accept_organisation_invite.py | 32 +++++++++++++++++++ app/organisation/invite_rest.py | 3 +- app/organisation/organisation_schema.py | 3 +- app/organisation/rest.py | 18 +++++++++++ tests/app/dao/test_organisation_dao.py | 10 ++++++ tests/app/invite/test_invite_rest.py | 1 - .../test_accept_organisation_invite.py | 18 +++++++++++ tests/app/organisation/test_invite_rest.py | 17 +++++----- 11 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 app/organisation/accept_organisation_invite.py create mode 100644 tests/app/organisation/test_accept_organisation_invite.py diff --git a/app/__init__.py b/app/__init__.py index c56ec476c..3c4a6823c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -109,6 +109,7 @@ def register_blueprint(application): from app.billing.rest import billing_blueprint from app.organisation.rest import organisation_blueprint from app.organisation.invite_rest import organisation_invite_blueprint + from app.organisation.accept_organisation_invite import accept_organisation_invite_blueprint service_blueprint.before_request(requires_admin_auth) application.register_blueprint(service_blueprint, url_prefix='/service') @@ -185,6 +186,9 @@ def register_blueprint(application): organisation_invite_blueprint.before_request(requires_admin_auth) application.register_blueprint(organisation_invite_blueprint) + accept_organisation_invite_blueprint.before_request(requires_admin_auth) + application.register_blueprint(accept_organisation_invite_blueprint) + def register_v2_blueprints(application): from app.v2.inbound_sms.get_inbound_sms import v2_inbound_sms_blueprint as get_inbound_sms diff --git a/app/dao/organisation_dao.py b/app/dao/organisation_dao.py index 50de10b07..446679dec 100644 --- a/app/dao/organisation_dao.py +++ b/app/dao/organisation_dao.py @@ -1,6 +1,6 @@ from app import db from app.dao.dao_utils import transactional -from app.models import Organisation +from app.models import Organisation, InvitedOrganisationUser, User def dao_get_organisations(): @@ -42,3 +42,23 @@ def dao_add_service_to_organisation(service, organisation_id): ).one() organisation.services.append(service) + + +def dao_get_invited_organisation_user(user_id): + return InvitedOrganisationUser.query.filter_by(id=user_id).first() + + +def dao_get_users_for_organisation(organisation_id): + return User.query.filter( + User.user_to_organisation.any(id=organisation_id), + User.state == 'active' + ).all() + + +def dao_add_user_to_organisation(organisation_id, user_id): + organisation = dao_get_organisation_by_id(organisation_id) + user = User.query.get(user_id) + organisation.users.append(user) + db.session.add(organisation) + db.session.commit() + return user diff --git a/app/models.py b/app/models.py index 4fb3a3094..baa8bc6b6 100644 --- a/app/models.py +++ b/app/models.py @@ -116,6 +116,10 @@ class User(db.Model): 'Service', secondary='user_to_service', backref=db.backref('user_to_service', lazy='dynamic')) + organisations = db.relationship( + 'Organisation', + secondary='user_to_organisation', + backref=db.backref('user_to_organisation', lazy='dynamic')) @property def password(self): @@ -245,8 +249,8 @@ class Organisation(db.Model): users = db.relationship( 'User', - secondary='user_to_organisation', - backref=db.backref('organisations', lazy='dynamic')) + secondary=user_to_organisation, + backref=db.backref('user_to_organisation', lazy='dynamic')) def serialize(self): serialized = { @@ -1447,10 +1451,10 @@ class InvitedOrganisationUser(db.Model): def serialize(self): return { - 'id': self.id, + 'id': str(self.id), 'email_address': self.email_address, - 'invited_by': self.invited_by_id, - 'organisation': self.organisation_id, + 'invited_by': str(self.invited_by_id), + 'organisation': str(self.organisation_id), 'created_at': self.created_at.strftime(DATETIME_FORMAT), 'status': self.status } diff --git a/app/organisation/accept_organisation_invite.py b/app/organisation/accept_organisation_invite.py new file mode 100644 index 000000000..c18e61df4 --- /dev/null +++ b/app/organisation/accept_organisation_invite.py @@ -0,0 +1,32 @@ +from flask import Blueprint, jsonify, current_app +from itsdangerous import SignatureExpired +from notifications_utils.url_safe_token import check_token + +from app.dao.organisation_dao import dao_get_invited_organisation_user +from app.errors import register_errors, InvalidRequest + +accept_organisation_invite_blueprint = Blueprint( + 'accept_organisation_invite', __name__, + url_prefix='/organisation-invitation') + +register_errors(accept_organisation_invite_blueprint) + + +@accept_organisation_invite_blueprint.route("/", methods=['GET']) +def accept_organisation_invitation(token): + max_age_seconds = 60 * 60 * 24 * current_app.config['INVITATION_EXPIRATION_DAYS'] + + try: + invited_user_id = check_token(token, + current_app.config['SECRET_KEY'], + current_app.config['DANGEROUS_SALT'], + max_age_seconds) + except SignatureExpired: + errors = {'invitation': + ['Your invitation to GOV.UK Notify has expired. ' + 'Please ask the person that invited you to send you another one']} + raise InvalidRequest(errors, status_code=400) + invited_user = dao_get_invited_organisation_user(invited_user_id) + + return jsonify(data=invited_user.serialize()), 200 + diff --git a/app/organisation/invite_rest.py b/app/organisation/invite_rest.py index ac7087e80..36c3602d1 100644 --- a/app/organisation/invite_rest.py +++ b/app/organisation/invite_rest.py @@ -73,7 +73,7 @@ def get_invited_org_users_by_organisation(organisation_id): @organisation_invite_blueprint.route('/', methods=['POST']) -def update_invite_status(organisation_id, invited_org_user_id): +def update_org_invite_status(organisation_id, invited_org_user_id): fetched = get_invited_org_user(organisation_id=organisation_id, invited_org_user_id=invited_org_user_id) data = request.get_json() @@ -81,6 +81,7 @@ def update_invite_status(organisation_id, invited_org_user_id): fetched.status = data['status'] save_invited_org_user(fetched) + return jsonify(data=fetched.serialize()), 200 diff --git a/app/organisation/organisation_schema.py b/app/organisation/organisation_schema.py index 48965682c..4b560fecc 100644 --- a/app/organisation/organisation_schema.py +++ b/app/organisation/organisation_schema.py @@ -1,3 +1,4 @@ +from app.models import INVITED_USER_STATUS_TYPES from app.schema_validation.definitions import uuid post_create_organisation_schema = { @@ -51,7 +52,7 @@ post_update_invited_org_user_status_schema = { "description": "POST update organisation invite schema", "type": "object", "properties": { - "status": {"type": "string"} + "status": {"enum": INVITED_USER_STATUS_TYPES} }, "required": ["status"] } diff --git a/app/organisation/rest.py b/app/organisation/rest.py index 6e29c9e89..124a82619 100644 --- a/app/organisation/rest.py +++ b/app/organisation/rest.py @@ -8,6 +8,8 @@ from app.dao.organisation_dao import ( dao_get_organisation_services, dao_update_organisation, dao_add_service_to_organisation, + dao_get_users_for_organisation, + dao_add_user_to_organisation ) from app.dao.services_dao import dao_fetch_service_by_id from app.errors import register_errors, InvalidRequest @@ -18,6 +20,7 @@ from app.organisation.organisation_schema import ( post_link_service_to_organisation_schema, ) from app.schema_validation import validate +from app.schemas import user_schema organisation_blueprint = Blueprint('organisation', __name__) register_errors(organisation_blueprint) @@ -90,3 +93,18 @@ def get_organisation_services(organisation_id): services = dao_get_organisation_services(organisation_id) sorted_services = sorted(services, key=lambda s: (-s.active, s.name)) return jsonify([s.serialize_for_org_dashboard() for s in sorted_services]) + + +@organisation_blueprint.route('//users/', methods=['POST']) +def add_user_to_organisation(organisation_id, user_id): + current_app.logger.info("ADDING new user") + new_org_user = dao_add_user_to_organisation(organisation_id, user_id) + return jsonify(data=user_schema.dump(new_org_user).data), 200 + + +@organisation_blueprint.route('//users', methods=['GET']) +def get_organisation_users(organisation_id): + org_users = dao_get_users_for_organisation(organisation_id) + + result = user_schema.dump(org_users, many=True) + return jsonify(data=result.data) diff --git a/tests/app/dao/test_organisation_dao.py b/tests/app/dao/test_organisation_dao.py index 04059cdf4..ad241f1c1 100644 --- a/tests/app/dao/test_organisation_dao.py +++ b/tests/app/dao/test_organisation_dao.py @@ -8,6 +8,7 @@ from app.dao.organisation_dao import ( dao_get_organisation_services, dao_update_organisation, dao_add_service_to_organisation, + dao_get_invited_organisation_user ) from app.models import Organisation @@ -104,3 +105,12 @@ def test_get_organisation_by_service_id(notify_db, notify_db_session, sample_ser assert organisation_1 == sample_organisation assert organisation_2 == another_org + + +def test_dao_get_invited_organisation_user(sample_invited_org_user): + invited_org_user = dao_get_invited_organisation_user(sample_invited_org_user.id) + assert invited_org_user == sample_invited_org_user + + +def test_dao_get_users_for_organisation(sample_organisation, sample_user): + dao \ No newline at end of file diff --git a/tests/app/invite/test_invite_rest.py b/tests/app/invite/test_invite_rest.py index 2a5450324..d7b89bc48 100644 --- a/tests/app/invite/test_invite_rest.py +++ b/tests/app/invite/test_invite_rest.py @@ -1,6 +1,5 @@ import json import pytest -import uuid from app.models import Notification, SMS_AUTH_TYPE, EMAIL_AUTH_TYPE from tests import create_authorization_header diff --git a/tests/app/organisation/test_accept_organisation_invite.py b/tests/app/organisation/test_accept_organisation_invite.py new file mode 100644 index 000000000..80f166129 --- /dev/null +++ b/tests/app/organisation/test_accept_organisation_invite.py @@ -0,0 +1,18 @@ +import json + +from flask import current_app +from notifications_utils.url_safe_token import generate_token + +from tests import create_authorization_header + + +def test_accept_organisation_invitation(client, sample_invited_org_user): + token = generate_token(str(sample_invited_org_user.id), current_app.config['SECRET_KEY'], + current_app.config['DANGEROUS_SALT']) + url = '/organisation-invitation/{}'.format(token) + auth_header = create_authorization_header() + response = client.get(url, headers=[('Content-Type', 'application/json'), auth_header]) + assert response.status_code == 200 + json_resp = json.loads(response.get_data(as_text=True)) + assert json_resp['data'] == sample_invited_org_user.serialize() + diff --git a/tests/app/organisation/test_invite_rest.py b/tests/app/organisation/test_invite_rest.py index ad83d36e0..b957ed912 100644 --- a/tests/app/organisation/test_invite_rest.py +++ b/tests/app/organisation/test_invite_rest.py @@ -109,11 +109,11 @@ def test_get_invited_users_by_service_with_no_invites(admin_request, sample_orga assert len(json_resp['data']) == 0 -def test_update_invited_user_set_status_to_cancelled(admin_request, sample_invited_org_user): +def test_update_org_invited_user_set_status_to_cancelled(admin_request, sample_invited_org_user): data = {'status': 'cancelled'} json_resp = admin_request.post( - 'organisation_invite.update_invite_status', + 'organisation_invite.update_org_invite_status', organisation_id=sample_invited_org_user.organisation_id, invited_org_user_id=sample_invited_org_user.id, _data=data @@ -121,11 +121,11 @@ def test_update_invited_user_set_status_to_cancelled(admin_request, sample_invit assert json_resp['data']['status'] == 'cancelled' -def test_update_invited_user_for_wrong_service_returns_404(admin_request, sample_invited_org_user, fake_uuid): +def test_update_org_invited_user_for_wrong_service_returns_404(admin_request, sample_invited_org_user, fake_uuid): data = {'status': 'cancelled'} json_resp = admin_request.post( - 'organisation_invite.update_invite_status', + 'organisation_invite.update_org_invite_status', organisation_id=fake_uuid, invited_org_user_id=sample_invited_org_user.id, _data=data, @@ -134,14 +134,15 @@ def test_update_invited_user_for_wrong_service_returns_404(admin_request, sample assert json_resp['message'] == 'No result found' -def test_update_invited_user_for_invalid_data_returns_400(admin_request, sample_invited_org_user): +def test_update_org_invited_user_for_invalid_data_returns_400(admin_request, sample_invited_org_user): data = {'status': 'garbage'} json_resp = admin_request.post( - 'organisation_invite.update_invite_status', + 'organisation_invite.update_org_invite_status', organisation_id=sample_invited_org_user.organisation_id, invited_org_user_id=sample_invited_org_user.id, _data=data, - _expected_status=404 + _expected_status=400 ) - assert json_resp['message'] == 'No result found' + assert len(json_resp['errors']) == 1 + assert json_resp['errors'][0]['message'] == 'status garbage is not one of [pending, accepted, cancelled]'