mirror of
https://github.com/GSA/notifications-api.git
synced 2026-08-14 10:58:06 -04:00
db update/insert. Using a savepoint for the multiple transactions allows us to rollback if there is an error when executing the second db transaction. However, this does add a bit of complexity. Developers need to manage the db session when calling multiple nested tranactions. Unit tests have been added to test this functionality and some end to end tests have been done to make sure all transactions are rollback if there is an exception while executing the transaction.
240 lines
8.5 KiB
Python
240 lines
8.5 KiB
Python
|
|
from flask import Blueprint, abort, current_app, jsonify, request
|
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
|
|
|
from app import db
|
|
from app.config import QueueNames
|
|
from app.dao.annual_billing_dao import set_default_free_allowance_for_service
|
|
from app.dao.fact_billing_dao import fetch_usage_year_for_organisation
|
|
from app.dao.organisation_dao import (
|
|
dao_add_service_to_organisation,
|
|
dao_add_user_to_organisation,
|
|
dao_create_organisation,
|
|
dao_get_organisation_by_email_address,
|
|
dao_get_organisation_by_id,
|
|
dao_get_organisation_services,
|
|
dao_get_organisations,
|
|
dao_get_users_for_organisation,
|
|
dao_update_organisation,
|
|
)
|
|
from app.dao.services_dao import dao_fetch_service_by_id
|
|
from app.dao.templates_dao import dao_get_template_by_id
|
|
from app.errors import InvalidRequest, register_errors
|
|
from app.models import KEY_TYPE_NORMAL, Organisation
|
|
from app.notifications.process_notifications import (
|
|
persist_notification,
|
|
send_notification_to_queue,
|
|
)
|
|
from app.organisation.organisation_schema import (
|
|
post_create_organisation_schema,
|
|
post_link_service_to_organisation_schema,
|
|
post_update_organisation_schema,
|
|
)
|
|
from app.schema_validation import validate
|
|
|
|
organisation_blueprint = Blueprint('organisation', __name__)
|
|
register_errors(organisation_blueprint)
|
|
|
|
|
|
@organisation_blueprint.errorhandler(IntegrityError)
|
|
def handle_integrity_error(exc):
|
|
"""
|
|
Handle integrity errors caused by the unique constraint on ix_organisation_name
|
|
"""
|
|
if 'ix_organisation_name' in str(exc):
|
|
return jsonify(result="error",
|
|
message="Organisation name already exists"), 400
|
|
if 'duplicate key value violates unique constraint "domain_pkey"' in str(exc):
|
|
return jsonify(result='error',
|
|
message='Domain already exists'), 400
|
|
|
|
current_app.logger.exception(exc)
|
|
return jsonify(result='error', message="Internal server error"), 500
|
|
|
|
|
|
@organisation_blueprint.route('', methods=['GET'])
|
|
def get_organisations():
|
|
organisations = [
|
|
org.serialize_for_list() for org in dao_get_organisations()
|
|
]
|
|
|
|
return jsonify(organisations)
|
|
|
|
|
|
@organisation_blueprint.route('/<uuid:organisation_id>', methods=['GET'])
|
|
def get_organisation_by_id(organisation_id):
|
|
organisation = dao_get_organisation_by_id(organisation_id)
|
|
return jsonify(organisation.serialize())
|
|
|
|
|
|
@organisation_blueprint.route('/by-domain', methods=['GET'])
|
|
def get_organisation_by_domain():
|
|
|
|
domain = request.args.get('domain')
|
|
|
|
if not domain or '@' in domain:
|
|
abort(400)
|
|
|
|
organisation = dao_get_organisation_by_email_address(
|
|
'example@{}'.format(request.args.get('domain'))
|
|
)
|
|
|
|
if not organisation:
|
|
abort(404)
|
|
|
|
return jsonify(organisation.serialize())
|
|
|
|
|
|
@organisation_blueprint.route('', methods=['POST'])
|
|
def create_organisation():
|
|
data = request.get_json()
|
|
|
|
validate(data, post_create_organisation_schema)
|
|
|
|
organisation = Organisation(**data)
|
|
dao_create_organisation(organisation)
|
|
return jsonify(organisation.serialize()), 201
|
|
|
|
|
|
@organisation_blueprint.route('/<uuid:organisation_id>', methods=['POST'])
|
|
def update_organisation(organisation_id):
|
|
data = request.get_json()
|
|
validate(data, post_update_organisation_schema)
|
|
result = dao_update_organisation(organisation_id, **data)
|
|
|
|
if data.get('agreement_signed') is True:
|
|
# if a platform admin has manually adjusted the organisation, don't tell people
|
|
if data.get('agreement_signed_by_id'):
|
|
send_notifications_on_mou_signed(organisation_id)
|
|
|
|
if result:
|
|
return '', 204
|
|
else:
|
|
raise InvalidRequest("Organisation not found", 404)
|
|
|
|
|
|
@organisation_blueprint.route('/<uuid:organisation_id>/service', methods=['POST'])
|
|
def link_service_to_organisation(organisation_id):
|
|
data = request.get_json()
|
|
validate(data, post_link_service_to_organisation_schema)
|
|
service = dao_fetch_service_by_id(data['service_id'])
|
|
service.organisation = None
|
|
|
|
try:
|
|
dao_add_service_to_organisation(service, organisation_id)
|
|
set_default_free_allowance_for_service(service, year_start=None)
|
|
db.session.commit()
|
|
except SQLAlchemyError as e:
|
|
db.session.rollback()
|
|
raise e
|
|
|
|
return '', 204
|
|
|
|
|
|
@organisation_blueprint.route('/<uuid:organisation_id>/services', methods=['GET'])
|
|
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('/<uuid:organisation_id>/services-with-usage', methods=['GET'])
|
|
def get_organisation_services_usage(organisation_id):
|
|
try:
|
|
year = int(request.args.get('year', 'none'))
|
|
except ValueError:
|
|
return jsonify(result='error', message='No valid year provided'), 400
|
|
services = fetch_usage_year_for_organisation(organisation_id, year)
|
|
list_services = services.values()
|
|
sorted_services = sorted(list_services, key=lambda s: (-s['active'], s['service_name'].lower()))
|
|
return jsonify(services=sorted_services)
|
|
|
|
|
|
@organisation_blueprint.route('/<uuid:organisation_id>/users/<uuid:user_id>', methods=['POST'])
|
|
def add_user_to_organisation(organisation_id, user_id):
|
|
new_org_user = dao_add_user_to_organisation(organisation_id, user_id)
|
|
return jsonify(data=new_org_user.serialize())
|
|
|
|
|
|
@organisation_blueprint.route('/<uuid:organisation_id>/users', methods=['GET'])
|
|
def get_organisation_users(organisation_id):
|
|
org_users = dao_get_users_for_organisation(organisation_id)
|
|
return jsonify(data=[x.serialize() for x in org_users])
|
|
|
|
|
|
@organisation_blueprint.route('/unique', methods=["GET"])
|
|
def is_organisation_name_unique():
|
|
organisation_id, name = check_request_args(request)
|
|
|
|
name_exists = Organisation.query.filter(Organisation.name.ilike(name)).first()
|
|
|
|
result = (not name_exists) or str(name_exists.id) == organisation_id
|
|
return jsonify(result=result), 200
|
|
|
|
|
|
def check_request_args(request):
|
|
org_id = request.args.get('org_id')
|
|
name = request.args.get('name', None)
|
|
errors = []
|
|
if not org_id:
|
|
errors.append({'org_id': ["Can't be empty"]})
|
|
if not name:
|
|
errors.append({'name': ["Can't be empty"]})
|
|
if errors:
|
|
raise InvalidRequest(errors, status_code=400)
|
|
return org_id, name
|
|
|
|
|
|
def send_notifications_on_mou_signed(organisation_id):
|
|
organisation = dao_get_organisation_by_id(organisation_id)
|
|
notify_service = dao_fetch_service_by_id(current_app.config['NOTIFY_SERVICE_ID'])
|
|
|
|
def _send_notification(template_id, recipient, personalisation):
|
|
template = dao_get_template_by_id(template_id)
|
|
|
|
saved_notification = persist_notification(
|
|
template_id=template.id,
|
|
template_version=template.version,
|
|
recipient=recipient,
|
|
service=notify_service,
|
|
personalisation=personalisation,
|
|
notification_type=template.template_type,
|
|
api_key_id=None,
|
|
key_type=KEY_TYPE_NORMAL,
|
|
reply_to_text=notify_service.get_default_reply_to_email_address()
|
|
)
|
|
send_notification_to_queue(saved_notification, research_mode=False, queue=QueueNames.NOTIFY)
|
|
|
|
personalisation = {
|
|
'mou_link': '{}/agreement/{}.pdf'.format(
|
|
current_app.config['ADMIN_BASE_URL'],
|
|
'crown' if organisation.crown else 'non-crown'
|
|
),
|
|
'org_name': organisation.name,
|
|
'org_dashboard_link': '{}/organisations/{}'.format(
|
|
current_app.config['ADMIN_BASE_URL'],
|
|
organisation.id
|
|
),
|
|
'signed_by_name': organisation.agreement_signed_by.name,
|
|
'on_behalf_of_name': organisation.agreement_signed_on_behalf_of_name
|
|
}
|
|
|
|
if not organisation.agreement_signed_on_behalf_of_email_address:
|
|
signer_template_id = 'MOU_SIGNER_RECEIPT_TEMPLATE_ID'
|
|
else:
|
|
signer_template_id = 'MOU_SIGNED_ON_BEHALF_SIGNER_RECEIPT_TEMPLATE_ID'
|
|
|
|
# let the person who has been signed on behalf of know.
|
|
_send_notification(
|
|
current_app.config['MOU_SIGNED_ON_BEHALF_ON_BEHALF_RECEIPT_TEMPLATE_ID'],
|
|
organisation.agreement_signed_on_behalf_of_email_address,
|
|
personalisation
|
|
)
|
|
|
|
# let the person who signed know - the template is different depending on if they signed on behalf of someone
|
|
_send_notification(
|
|
current_app.config[signer_template_id],
|
|
organisation.agreement_signed_by.email_address,
|
|
personalisation
|
|
)
|