Merge pull request #3120 from alphagov/update-service-broadcast-settings

Update service broadcast settings
This commit is contained in:
David McDonald
2021-02-16 14:50:18 +00:00
committed by GitHub
7 changed files with 569 additions and 8 deletions

View File

@@ -384,6 +384,9 @@ class Config(object):
ENABLED_CBCS = {BroadcastProvider.EE, BroadcastProvider.THREE, BroadcastProvider.O2, BroadcastProvider.VODAFONE}
# as defined in api db migration 0331_add_broadcast_org.py
BROADCAST_ORGANISATION_ID = '38e4bf69-93b0-445d-acee-53ea53fe02df'
######################
# Config overrides ###

View File

@@ -0,0 +1,65 @@
from datetime import datetime
from flask import current_app
from app import db
from app.models import ServiceBroadcastSettings, ServicePermission, Organisation, BROADCAST_TYPE
from app.dao.dao_utils import transactional
@transactional
def set_broadcast_service_type(service, service_mode, broadcast_channel, provider_restriction):
insert_or_update_service_broadcast_settings(
service, channel=broadcast_channel, provider_restriction=provider_restriction
)
# Remove all permissions and add broadcast permission
if not service.has_permission(BROADCAST_TYPE):
service_permission = ServicePermission(service_id=service.id, permission=BROADCAST_TYPE)
db.session.add(service_permission)
ServicePermission.query.filter(
ServicePermission.service_id == service.id,
ServicePermission.permission != BROADCAST_TYPE
).delete()
# Refresh the service object as it has references to the service permissions but we don't yet
# want to commit the permission changes incase all of this needs to rollback
db.session.refresh(service)
# Set service count as live false always
service.count_as_live = False
# Set service into training mode or live mode
if service_mode == "live":
if service.restricted:
# Only update the go live at timestamp if this if moving from training mode
# to live mode, not if it's moving from one type of live mode service to another
service.go_live_at = datetime.utcnow()
service.restricted = False
else:
service.restricted = True
service.go_live_at = None
# Add service to organisation
organisation = Organisation.query.filter_by(
id=current_app.config['BROADCAST_ORGANISATION_ID']
).one()
service.organisation_id = organisation.id
service.organisation_type = organisation.organisation_type
service.crown = organisation.crown
db.session.add(service)
def insert_or_update_service_broadcast_settings(service, channel, provider_restriction=None):
if not service.service_broadcast_settings:
settings = ServiceBroadcastSettings()
settings.service = service
settings.channel = channel
settings.provider = provider_restriction
db.session.add(settings)
else:
service.service_broadcast_settings.channel = channel
service.service_broadcast_settings.provider = provider_restriction
db.session.add(service.service_broadcast_settings)

View File

@@ -25,7 +25,7 @@ from notifications_utils.recipients import (
from app import ma
from app import models
from app.models import ServicePermission
from app.models import ServicePermission, BROADCAST_TYPE
from app.dao.permissions_dao import permission_dao
from app.utils import DATETIME_FORMAT_NO_TIMEZONE, get_template_instance
@@ -236,10 +236,21 @@ class ServiceSchema(BaseSchema, UUIDsAsStringsMixin):
override_flag = False
go_live_at = field_for(models.Service, 'go_live_at', format=DATETIME_FORMAT_NO_TIMEZONE)
allowed_broadcast_provider = fields.Method(dump_only=True, serialize='_get_allowed_broadcast_provider')
broadcast_channel = fields.Method(dump_only=True, serialize='_get_broadcast_channel')
def _get_allowed_broadcast_provider(self, service):
return service.allowed_broadcast_provider
def _get_broadcast_channel(self, service):
# TODO: Once we've migrated data so that all broadcast services have `service.broadcast_channel`
# set then we can remove this logic and related tests and instead just return
# `service.broadcast_channel`. For the moment though, as we have some services with the broadcast
# permission that do not have a row in the service_broadcast_settings table, we need to hardcode
# this in here to give them a default that the admin app can use
if BROADCAST_TYPE in self.service_permissions(service):
return service.broadcast_channel if service.broadcast_channel else "test"
return None
def get_letter_logo_filename(self, service):
return service.letter_branding and service.letter_branding.filename

View File

@@ -29,7 +29,9 @@ from app.dao.fact_notification_status_dao import (
fetch_stats_for_all_services_by_date_range, fetch_monthly_template_usage_for_service
)
from app.dao.inbound_numbers_dao import dao_allocate_number_for_service
from app.dao.organisation_dao import dao_get_organisation_by_service_id
from app.dao.organisation_dao import (
dao_get_organisation_by_service_id,
)
from app.dao.returned_letters_dao import (
fetch_most_recent_returned_letter,
fetch_recent_returned_letter_count,
@@ -42,6 +44,7 @@ from app.dao.service_contact_list_dao import (
dao_get_contact_list_by_id,
save_service_contact_list,
)
from app.dao.broadcast_service_dao import set_broadcast_service_type
from app.dao.service_data_retention_dao import (
fetch_service_data_retention,
fetch_service_data_retention_by_id,
@@ -100,8 +103,13 @@ from app.errors import (
)
from app.letters.utils import letter_print_day
from app.models import (
KEY_TYPE_NORMAL, LETTER_TYPE, NOTIFICATION_CANCELLED, Permission, Service,
EmailBranding, LetterBranding,
KEY_TYPE_NORMAL,
LETTER_TYPE,
NOTIFICATION_CANCELLED,
Permission,
Service,
EmailBranding,
LetterBranding,
ServiceContactList
)
from app.notifications.process_notifications import persist_notification, send_notification_to_queue
@@ -118,6 +126,7 @@ from app.service.service_senders_schema import (
add_service_letter_contact_block_request,
add_service_sms_sender_request
)
from app.service.service_broadcast_settings_schema import service_broadcast_settings_schema
from app.service.utils import get_guest_list_objects
from app.service.sender import send_notification_to_service_users
from app.service.send_notification import send_one_off_notification, send_pdf_letter_notification
@@ -1070,3 +1079,27 @@ def create_contact_list(service_id):
save_service_contact_list(list_to_save)
return jsonify(list_to_save.serialize()), 201
@service_blueprint.route('/<uuid:service_id>/set-as-broadcast-service', methods=['POST'])
def set_as_broadcast_service(service_id):
"""
This route does the following
- adds a service broadcast settings to define which channel broadcasts should go out on
- removes all current service permissions and adds the broadcast service permission
- sets the services `count_as_live` to false
- adds the service to the broadcast organisation
- puts the service into training mode or live mode
"""
data = validate(request.get_json(), service_broadcast_settings_schema)
service = dao_fetch_service_by_id(service_id)
set_broadcast_service_type(
service,
service_mode=data["service_mode"],
broadcast_channel=data["broadcast_channel"],
provider_restriction=data["provider_restriction"]
)
data = service_schema.dump(service).data
return jsonify(data=data)

View File

@@ -0,0 +1,12 @@
service_broadcast_settings_schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Set a services broadcast settings",
"type": "object",
"title": "Set a services broadcast settings",
"properties": {
"broadcast_channel": {"enum": ["test", "severe"]},
"service_mode": {"enum": ["training", "live"]},
"provider_restriction": {"enum": [None, "three", "o2", "vodafone", "ee"]}
},
"required": ["broadcast_channel", "service_mode", "provider_restriction"]
}

View File

@@ -14,8 +14,9 @@ from app.dao.api_key_dao import save_model_api_key
from app.dao.invited_user_dao import save_invited_user
from app.dao.jobs_dao import dao_create_job
from app.dao.notifications_dao import dao_create_notification
from app.dao.organisation_dao import dao_create_organisation
from app.dao.organisation_dao import dao_create_organisation, dao_add_service_to_organisation
from app.dao.services_dao import (dao_create_service, dao_add_user_to_service)
from app.dao.broadcast_service_dao import insert_or_update_service_broadcast_settings
from app.dao.templates_dao import dao_create_template
from app.dao.users_dao import create_secret_code, create_user_code
from app.history_meta import create_history
@@ -42,7 +43,7 @@ from app.models import (
LETTER_TYPE,
SERVICE_PERMISSION_TYPES,
ServiceEmailReplyTo,
BROADCAST_TYPE
BROADCAST_TYPE,
)
from tests import create_authorization_header
from tests.app.db import (
@@ -150,7 +151,7 @@ def sample_service(notify_db_session):
@pytest.fixture(scope='function')
def sample_broadcast_service(notify_db_session):
def sample_broadcast_service(notify_db_session, broadcast_organisation):
user = create_user()
service_name = 'Sample broadcast service'
email_from = service_name.lower().replace(' ', '.')
@@ -161,12 +162,15 @@ def sample_broadcast_service(notify_db_session):
'restricted': False,
'email_from': email_from,
'created_by': user,
'crown': True
'crown': True,
'count_as_live': False,
}
service = Service.query.filter_by(name=service_name).first()
if not service:
service = Service(**data)
dao_create_service(service, user, service_permissions=[BROADCAST_TYPE])
insert_or_update_service_broadcast_settings(service, channel="severe")
dao_add_service_to_organisation(service, current_app.config['BROADCAST_ORGANISATION_ID'])
else:
if user not in service.users:
dao_add_user_to_service(service, user)
@@ -874,6 +878,16 @@ def sample_organisation(notify_db_session):
return org
@pytest.fixture
def broadcast_organisation(notify_db_session):
org = Organisation.query.get(current_app.config['BROADCAST_ORGANISATION_ID'])
if not org:
org = Organisation(id=current_app.config['BROADCAST_ORGANISATION_ID'], name='broadcast organisation')
dao_create_organisation(org)
return org
@pytest.fixture
def restore_provider_details(notify_db, notify_db_session):
"""

View File

@@ -31,6 +31,7 @@ from app.models import (
EMAIL_TYPE,
SMS_TYPE,
LETTER_TYPE,
BROADCAST_TYPE,
INTERNATIONAL_LETTERS,
INTERNATIONAL_SMS_TYPE,
INBOUND_SMS_TYPE,
@@ -243,6 +244,7 @@ def test_get_service_by_id(admin_request, sample_service):
assert json_resp['data']['email_branding'] is None
assert json_resp['data']['prefix_sms'] is True
assert json_resp['data']['allowed_broadcast_provider'] is None
assert json_resp['data']['broadcast_channel'] is None
assert set(json_resp['data'].keys()) == {
'active',
@@ -250,6 +252,7 @@ def test_get_service_by_id(admin_request, sample_service):
'billing_contact_email_addresses',
'billing_contact_names',
'billing_reference',
'broadcast_channel',
'consent_to_research',
'contact_link',
'count_as_live',
@@ -288,6 +291,38 @@ def test_get_service_by_id_returns_allowed_broadcast_provider(notify_db, admin_r
assert json_resp['data']['allowed_broadcast_provider'] == 'ee'
def test_get_service_by_id_for_broadcast_service_takes_channel_from_service_broadcast_settings(
admin_request, sample_broadcast_service
):
assert sample_broadcast_service.broadcast_channel == 'severe'
json_resp = admin_request.get('service.get_service_by_id', service_id=sample_broadcast_service.id)
assert json_resp['data']['id'] == str(sample_broadcast_service.id)
assert json_resp['data']['broadcast_channel'] == 'severe'
def test_get_service_by_id_for_service_with_broadcast_permission_sets_channel_as_test_if_no_service_broadcast_settings(
admin_request, notify_db_session
):
service = create_service(service_permissions=[BROADCAST_TYPE])
assert BROADCAST_TYPE in [p.permission for p in service.permissions]
assert service.broadcast_channel is None
json_resp = admin_request.get('service.get_service_by_id', service_id=service.id)
assert json_resp['data']['id'] == str(service.id)
assert json_resp['data']['broadcast_channel'] == 'test'
def test_get_service_by_id_for_non_broadcast_service_sets_channel_as_none(
admin_request, sample_service
):
assert BROADCAST_TYPE not in [p.permission for p in sample_service.permissions]
json_resp = admin_request.get('service.get_service_by_id', service_id=sample_service.id)
assert json_resp['data']['id'] == str(sample_service.id)
assert json_resp['data']['broadcast_channel'] is None
@pytest.mark.parametrize('detailed', [True, False])
def test_get_service_by_id_returns_organisation_type(admin_request, sample_service, detailed):
json_resp = admin_request.get('service.get_service_by_id', service_id=sample_service.id, detailed=detailed)
@@ -3645,3 +3680,391 @@ def test_get_returned_letter(admin_request, sample_letter_template):
assert not response[4]['original_file_name']
assert not response[4]['job_row_number']
assert response[4]['uploaded_letter_file_name'] == 'filename.pdf'
@pytest.mark.parametrize('channel', ["test", "severe"])
def test_set_as_broadcast_service_sets_broadcast_channel(
admin_request, sample_service, broadcast_organisation, channel
):
assert sample_service.service_broadcast_settings is None
data = {
'broadcast_channel': channel,
'service_mode': 'live',
'provider_restriction': None,
}
result = admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
)
assert result['data']['name'] == 'Sample service'
assert result['data']['broadcast_channel'] == channel
records = ServiceBroadcastSettings.query.filter_by(service_id=sample_service.id).all()
assert len(records) == 1
assert records[0].service_id == sample_service.id
assert records[0].channel == channel
def test_set_as_broadcast_service_updates_channel_for_broadcast_service(
admin_request, sample_broadcast_service
):
assert sample_broadcast_service.broadcast_channel == "severe"
data = {
'broadcast_channel': "test",
'service_mode': 'training',
'provider_restriction': None,
}
result = admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_broadcast_service.id,
_data=data,
)
assert result['data']['name'] == 'Sample broadcast service'
assert result['data']['broadcast_channel'] == "test"
records = ServiceBroadcastSettings.query.filter_by(service_id=sample_broadcast_service.id).all()
assert len(records) == 1
assert records[0].service_id == sample_broadcast_service.id
assert records[0].channel == "test"
@pytest.mark.parametrize('channel', ["government", "extreme", "exercise", "random", ""])
def test_set_as_broadcast_service_rejects_unknown_channels(
admin_request, sample_service, broadcast_organisation, channel
):
data = {
'broadcast_channel': channel,
'service_mode': 'live',
'provider_restriction': None,
}
admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
_expected_status=400,
)
def test_set_as_broadcast_service_rejects_if_no_channel(
admin_request, notify_db, sample_service, broadcast_organisation
):
data = {
'service_mode': 'training',
'provider_restriction': None,
}
admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
_expected_status=400,
)
def test_set_as_broadcast_service_gives_broadcast_permission_and_removes_other_permissions(
admin_request, sample_service, broadcast_organisation
):
current_permissions = [p.permission for p in sample_service.permissions]
assert len(current_permissions) > 0
assert BROADCAST_TYPE not in current_permissions
data = {
'broadcast_channel': "severe",
'service_mode': 'training',
'provider_restriction': None,
}
result = admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
)
assert result['data']['permissions'] == [BROADCAST_TYPE]
permissions = ServicePermission.query.filter_by(service_id=sample_service.id).all()
assert [p.permission for p in permissions] == [BROADCAST_TYPE]
def test_set_as_broadcast_service_maintains_broadcast_permission_for_existing_broadcast_service(
admin_request, sample_broadcast_service
):
current_permissions = [p.permission for p in sample_broadcast_service.permissions]
assert current_permissions == [BROADCAST_TYPE]
data = {
'broadcast_channel': "severe",
'service_mode': 'live',
'provider_restriction': None,
}
result = admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_broadcast_service.id,
_data=data,
)
assert result['data']['permissions'] == [BROADCAST_TYPE]
permissions = ServicePermission.query.filter_by(service_id=sample_broadcast_service.id).all()
assert [p.permission for p in permissions] == [BROADCAST_TYPE]
def test_set_as_broadcast_service_sets_count_as_live_to_false(
admin_request, sample_service, broadcast_organisation
):
assert sample_service.count_as_live is True
data = {
'broadcast_channel': "severe",
'service_mode': 'live',
'provider_restriction': None,
}
result = admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
)
assert result['data']['count_as_live'] is False
service_from_db = Service.query.filter_by(id=sample_service.id).all()[0]
assert service_from_db.count_as_live is False
def test_set_as_broadcast_service_sets_service_org_to_broadcast_org(
admin_request, sample_service, broadcast_organisation
):
assert sample_service.organisation_id != current_app.config['BROADCAST_ORGANISATION_ID']
data = {
'broadcast_channel': "severe",
'service_mode': 'training',
'provider_restriction': None,
}
result = admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
)
assert result['data']['organisation'] == current_app.config['BROADCAST_ORGANISATION_ID']
service_from_db = Service.query.filter_by(id=sample_service.id).all()[0]
assert str(service_from_db.organisation_id) == current_app.config['BROADCAST_ORGANISATION_ID']
def test_set_as_broadcast_service_does_not_error_if_run_on_a_service_that_is_already_a_broadcast_service(
admin_request, sample_service, broadcast_organisation
):
data = {
'broadcast_channel': "severe",
'service_mode': "live",
'provider_restriction': None,
}
for _ in range(2):
admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
)
@freeze_time('2021-02-02')
def test_set_as_broadcast_service_sets_service_to_live_mode(
admin_request, notify_db, sample_service, broadcast_organisation
):
sample_service.restricted = True
notify_db.session.add(sample_service)
notify_db.session.commit()
assert sample_service.restricted is True
assert sample_service.go_live_at is None
data = {
'broadcast_channel': 'severe',
'service_mode': 'live',
'provider_restriction': None,
}
result = admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
)
assert result['data']['name'] == 'Sample service'
assert result['data']['restricted'] is False
assert result['data']['go_live_at'] == '2021-02-02 00:00:00.000000'
def test_set_as_broadcast_service_doesnt_override_existing_go_live_at(
admin_request, notify_db, sample_broadcast_service
):
sample_broadcast_service.restricted = False
sample_broadcast_service.go_live_at = datetime(2021, 1, 1)
notify_db.session.add(sample_broadcast_service)
notify_db.session.commit()
assert sample_broadcast_service.restricted is False
assert sample_broadcast_service.go_live_at is not None
data = {
'broadcast_channel': 'severe',
'service_mode': 'live',
'provider_restriction': None,
}
result = admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_broadcast_service.id,
_data=data,
)
assert result['data']['name'] == 'Sample broadcast service'
assert result['data']['restricted'] is False
assert result['data']['go_live_at'] == '2021-01-01 00:00:00.000000'
def test_set_as_broadcast_service_sets_service_to_training_mode(
admin_request, notify_db, sample_broadcast_service
):
sample_broadcast_service.restricted = False
sample_broadcast_service.go_live_at = datetime(2021, 1, 1)
notify_db.session.add(sample_broadcast_service)
notify_db.session.commit()
assert sample_broadcast_service.restricted is False
assert sample_broadcast_service.go_live_at is not None
data = {
'broadcast_channel': 'severe',
'service_mode': 'training',
'provider_restriction': None,
}
result = admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_broadcast_service.id,
_data=data,
)
assert result['data']['name'] == 'Sample broadcast service'
assert result['data']['restricted'] is True
assert result['data']['go_live_at'] is None
@pytest.mark.parametrize('service_mode', ["testing", ""])
def test_set_as_broadcast_service_rejects_unknown_service_mode(
admin_request, sample_service, broadcast_organisation, service_mode
):
data = {
'broadcast_channel': 'severe',
'service_mode': service_mode,
'provider_restriction': None,
}
admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
_expected_status=400,
)
def test_set_as_broadcast_service_rejects_if_no_service_mode(
admin_request, sample_service, broadcast_organisation
):
data = {
'broadcast_channel': 'severe',
'provider_restriction': None,
}
admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
_expected_status=400,
)
@pytest.mark.parametrize('provider', [None, "three", "ee", "vodafone", "o2"])
def test_set_as_broadcast_service_sets_mobile_provider_restriction(
admin_request, sample_service, broadcast_organisation, provider
):
assert sample_service.service_broadcast_settings is None
data = {
'broadcast_channel': 'severe',
'service_mode': 'live',
'provider_restriction': provider
}
result = admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
)
assert result['data']['name'] == 'Sample service'
assert result['data']['allowed_broadcast_provider'] == provider
records = ServiceBroadcastSettings.query.filter_by(service_id=sample_service.id).all()
assert len(records) == 1
assert records[0].service_id == sample_service.id
assert records[0].provider == provider
@pytest.mark.parametrize('provider', [None, "vodafone"])
def test_set_as_broadcast_service_updates_mobile_provider_restriction(
admin_request, notify_db, sample_broadcast_service, provider
):
sample_broadcast_service.service_broadcast_settings.provider = "o2"
notify_db.session.add(sample_broadcast_service)
notify_db.session.commit()
assert sample_broadcast_service.service_broadcast_settings.provider == "o2"
data = {
'broadcast_channel': 'severe',
'service_mode': 'live',
'provider_restriction': provider
}
result = admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_broadcast_service.id,
_data=data,
)
assert result['data']['name'] == 'Sample broadcast service'
assert result['data']['allowed_broadcast_provider'] == provider
records = ServiceBroadcastSettings.query.filter_by(service_id=sample_broadcast_service.id).all()
assert len(records) == 1
assert records[0].service_id == sample_broadcast_service.id
assert records[0].provider == provider
@pytest.mark.parametrize('provider', ["three, o2", "giffgaff", "", "None"])
def test_set_as_broadcast_service_rejects_unknown_provider_restriction(
admin_request, sample_service, broadcast_organisation, provider
):
data = {
'broadcast_channel': 'test',
'service_mode': 'live',
'provider_restriction': provider
}
admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
_expected_status=400,
)
def test_set_as_broadcast_service_errors_if_no_mobile_provider_restriction(
admin_request, sample_service, broadcast_organisation
):
data = {
'broadcast_channel': 'severe',
'service_mode': 'live',
}
admin_request.post(
'service.set_as_broadcast_service',
service_id=sample_service.id,
_data=data,
_expected_status=400,
)