From 42032f871f18ab6e98b573712af06c1b262380e3 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Thu, 4 May 2017 14:57:57 +0100 Subject: [PATCH 01/15] removed Flask-Redis dependency, it's included in notification-utils. --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3bc64fbc2..3f688a907 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,6 @@ celery==3.1.23 monotonic==1.2 statsd==3.2.1 jsonschema==2.5.1 -Flask-Redis==0.1.0 gunicorn==19.6.0 docopt==0.6.2 six==1.10.0 From 34e6ab3211edea8a68b8f81c6fe5563ebd014cff Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Thu, 4 May 2017 17:02:17 +0100 Subject: [PATCH 02/15] add new notification_status_types static table enums are hard to develop with - it is a slow and unchunkable change to add a new type or remove an old type. We're slowly moving away from using enums, in favour of static type tables, where the name is the primary key. This commit contains an alembic script that creates that table, and creates a new nullable column on notifications and notification_history that refer to it. --- .../versions/0081_noti_status_as_enum.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 migrations/versions/0081_noti_status_as_enum.py diff --git a/migrations/versions/0081_noti_status_as_enum.py b/migrations/versions/0081_noti_status_as_enum.py new file mode 100644 index 000000000..40dfd9f1e --- /dev/null +++ b/migrations/versions/0081_noti_status_as_enum.py @@ -0,0 +1,33 @@ +"""empty message + +Revision ID: 0081_noti_status_as_enum +Revises: 0080_fix_rate_start_date +Create Date: 2017-05-02 14:50:04.070874 + +""" + +# revision identifiers, used by Alembic. +revision = '0081_noti_status_as_enum' +down_revision = '0080_fix_rate_start_date' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('notification_status', + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('name') + ) + + op.execute('ALTER TABLE notifications ADD COLUMN notification_status text') + op.execute('ALTER TABLE notification_history ADD COLUMN notification_status text') + + op.create_index(op.f('ix_notifications_notification_status'), 'notifications', ['notification_status']) + op.create_index(op.f('ix_notification_history_notification_status'), 'notification_history', ['notification_status']) + + +def downgrade(): + op.execute('ALTER TABLE notifications DROP COLUMN notification_status') + op.execute('ALTER TABLE notification_history DROP COLUMN notification_status') + op.execute('DROP TABLE notification_status') From 7e52fa4d138f9ddba5f0428f89bebbf648c8ce7b Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Thu, 4 May 2017 17:09:04 +0100 Subject: [PATCH 03/15] add new notification_status column to models.py We now have a new column in the database, but it isn't being populated. The first step is to make sure we update this column, while still keeping the old enum based column up to date as well. A couple of changes have had to happen to support this - one irritating thing is that if we're ever querying columns individually, including `Notification.status`, then we'll need to give that column a label, since under the hood it translates to `Notification._status_enum`. Accessing status through the ORM (i.e., my_noti.status = 'sending' or similar) will work fine. --- app/dao/services_dao.py | 22 +++++++---- app/errors.py | 2 +- app/models.py | 55 ++++++++++++++++++++++++-- tests/app/dao/test_notification_dao.py | 24 ++++++++++- tests/app/service/test_rest.py | 11 +++--- tests/conftest.py | 3 +- 6 files changed, 96 insertions(+), 21 deletions(-) diff --git a/app/dao/services_dao.py b/app/dao/services_dao.py index f55b6ec4c..c5679289c 100644 --- a/app/dao/services_dao.py +++ b/app/dao/services_dao.py @@ -127,7 +127,8 @@ def dao_update_service(service): db.session.add(service) -def dao_add_user_to_service(service, user, permissions=[]): +def dao_add_user_to_service(service, user, permissions=None): + permissions = permissions or [] try: from app.dao.permissions_dao import permission_dao service.users.append(user) @@ -214,7 +215,8 @@ def fetch_todays_total_message_count(service_id): def _stats_for_service_query(service_id): return db.session.query( Notification.notification_type, - Notification.status, + # see dao_fetch_todays_stats_for_all_services for why we have this label + Notification.status.label('status'), func.count(Notification.id).label('count') ).filter( Notification.service_id == service_id, @@ -232,13 +234,13 @@ def dao_fetch_monthly_historical_stats_by_template_for_service(service_id, year) start_date, end_date = get_financial_year(year) sq = db.session.query( NotificationHistory.template_id, - NotificationHistory.status, + # see dao_fetch_todays_stats_for_all_services for why we have this label + NotificationHistory.status.label('status'), month.label('month'), func.count().label('count') ).filter( NotificationHistory.service_id == service_id, NotificationHistory.created_at.between(start_date, end_date) - ).group_by( month, NotificationHistory.template_id, @@ -249,7 +251,7 @@ def dao_fetch_monthly_historical_stats_by_template_for_service(service_id, year) Template.id.label('template_id'), Template.name, Template.template_type, - sq.c.status, + sq.c.status.label('status'), sq.c.count.label('count'), sq.c.month ).join( @@ -267,7 +269,8 @@ def dao_fetch_monthly_historical_stats_for_service(service_id, year): start_date, end_date = get_financial_year(year) rows = db.session.query( NotificationHistory.notification_type, - NotificationHistory.status, + # see dao_fetch_todays_stats_for_all_services for why we have this label + NotificationHistory.status.label('status'), month, func.count(NotificationHistory.id).label('count') ).filter( @@ -306,7 +309,9 @@ def dao_fetch_monthly_historical_stats_for_service(service_id, year): def dao_fetch_todays_stats_for_all_services(include_from_test_key=True): query = db.session.query( Notification.notification_type, - Notification.status, + # this label is necessary as the column has a different name under the hood (_status_enum / _status_fkey), + # if we query the Notification object there is a hybrid property to translate, but here there isn't anything. + Notification.status.label('status'), Notification.service_id, func.count(Notification.id).label('count') ).filter( @@ -336,7 +341,8 @@ def fetch_stats_by_date_range_for_all_services(start_date, end_date, include_fro query = db.session.query( table.notification_type, - table.status, + # see dao_fetch_todays_stats_for_all_services for why we have this label + table.status.label('status'), table.service_id, func.count(table.id).label('count') ).filter( diff --git a/app/errors.py b/app/errors.py index 1c9c1691d..7e7790bc8 100644 --- a/app/errors.py +++ b/app/errors.py @@ -92,7 +92,7 @@ def register_errors(blueprint): @blueprint.errorhandler(SQLAlchemyError) def db_error(e): current_app.logger.exception(e) - if e.orig.pgerror and \ + if hasattr(e, 'orig') and hasattr(e.orig, 'pgerror') and e.orig.pgerror and \ ('duplicate key value violates unique constraint "services_name_key"' in e.orig.pgerror or 'duplicate key value violates unique constraint "services_email_from_key"' in e.orig.pgerror): return jsonify( diff --git a/app/models.py b/app/models.py index 0ef2de359..605b7bab8 100644 --- a/app/models.py +++ b/app/models.py @@ -1,8 +1,9 @@ import time import uuid import datetime -from flask import url_for +from flask import url_for, current_app +from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.dialects.postgresql import ( UUID, JSON @@ -46,7 +47,12 @@ class HistoryModel: def update_from_original(self, original): for c in self.__table__.columns: - setattr(self, c.name, getattr(original, c.name)) + # in some cases, columns may have different names to their underlying db column - so only copy those + # that we can, and leave it up to subclasses to deal with any oddities/properties etc. + if hasattr(original, c.name): + setattr(self, c.name, getattr(original, c.name)) + else: + current_app.logger.debug('{} has no column {} to copy from'.format(original, c.name)) class User(db.Model): @@ -621,6 +627,12 @@ NOTIFICATION_STATUS_TYPES = [ NOTIFICATION_STATUS_TYPES_ENUM = db.Enum(*NOTIFICATION_STATUS_TYPES, name='notify_status_type') +class NotificationStatusTypes(db.Model): + __tablename__ = 'notification_status_types' + + name = db.Column(db.String(255), primary_key=True) + + class Notification(db.Model): __tablename__ = 'notifications' @@ -656,7 +668,15 @@ class Notification(db.Model): unique=False, nullable=True, onupdate=datetime.datetime.utcnow) - status = db.Column(NOTIFICATION_STATUS_TYPES_ENUM, index=True, nullable=False, default='created') + _status_enum = db.Column('status', NOTIFICATION_STATUS_TYPES_ENUM, index=True, nullable=False, default='created') + _status_fkey = db.Column( + 'notification_status', + db.String, + db.ForeignKey('notification_status_types.name'), + index=True, + nullable=True, + default='created' + ) reference = db.Column(db.String, nullable=True, index=True) client_reference = db.Column(db.String, index=True, nullable=True) _personalisation = db.Column(db.String, nullable=True) @@ -672,6 +692,15 @@ class Notification(db.Model): phone_prefix = db.Column(db.String, nullable=True) rate_multiplier = db.Column(db.Float(asdecimal=False), nullable=True) + @hybrid_property + def status(self): + return self._status_enum + + @status.setter + def status(self, status): + self._status_fkey = status + self._status_enum = status + @property def personalisation(self): if self._personalisation: @@ -844,7 +873,15 @@ class NotificationHistory(db.Model, HistoryModel): sent_at = db.Column(db.DateTime, index=False, unique=False, nullable=True) sent_by = db.Column(db.String, nullable=True) updated_at = db.Column(db.DateTime, index=False, unique=False, nullable=True) - status = db.Column(NOTIFICATION_STATUS_TYPES_ENUM, index=True, nullable=False, default='created') + _status_enum = db.Column('status', NOTIFICATION_STATUS_TYPES_ENUM, index=True, nullable=False, default='created') + _status_fkey = db.Column( + 'notification_status', + db.String, + db.ForeignKey('notification_status_types.name'), + index=True, + nullable=True, + default='created' + ) reference = db.Column(db.String, nullable=True, index=True) client_reference = db.Column(db.String, nullable=True) @@ -855,8 +892,18 @@ class NotificationHistory(db.Model, HistoryModel): @classmethod def from_original(cls, notification): history = super().from_original(notification) + history.status = notification.status return history + @hybrid_property + def status(self): + return self._status_enum + + @status.setter + def status(self, status): + self._status_fkey = status + self._status_enum = status + INVITED_USER_STATUS_TYPES = ['pending', 'accepted', 'cancelled'] diff --git a/tests/app/dao/test_notification_dao.py b/tests/app/dao/test_notification_dao.py index b5461e036..9c5006de7 100644 --- a/tests/app/dao/test_notification_dao.py +++ b/tests/app/dao/test_notification_dao.py @@ -311,6 +311,8 @@ def test_should_by_able_to_update_status_by_id(sample_template, sample_job, mmg_ data = _notification_json(sample_template, job_id=sample_job.id, status='sending') notification = Notification(**data) dao_create_notification(notification) + assert notification._status_enum == 'sending' + assert notification._status_fkey == 'sending' assert Notification.query.get(notification.id).status == 'sending' @@ -321,6 +323,8 @@ def test_should_by_able_to_update_status_by_id(sample_template, sample_job, mmg_ assert updated.updated_at == datetime(2000, 1, 2, 12, 0, 0) assert Notification.query.get(notification.id).status == 'delivered' assert notification.updated_at == datetime(2000, 1, 2, 12, 0, 0) + assert notification._status_enum == 'delivered' + assert notification._status_fkey == 'delivered' def test_should_not_update_status_by_id_if_not_sending_and_does_not_update_job(notify_db, notify_db_session): @@ -825,7 +829,7 @@ def test_get_notification_billable_unit_count_per_month(notify_db, notify_db_ses ) == months -def test_update_notification(sample_notification, sample_template): +def test_update_notification(sample_notification): assert sample_notification.status == 'created' sample_notification.status = 'failed' dao_update_notification(sample_notification) @@ -833,6 +837,24 @@ def test_update_notification(sample_notification, sample_template): assert notification_from_db.status == 'failed' +def test_update_notification_with_no_notification_status(sample_notification): + # specifically, it has an old enum status, but not a new status (because the upgrade script has just run) + sample_notification._status_fkey = None + sample_notification._enum_status = 'created' + dao_update_notification(sample_notification) + + assert sample_notification.status == 'created' + assert sample_notification._enum_status == 'created' + assert sample_notification._status_fkey == None + + sample_notification.status = 'failed' + dao_update_notification(sample_notification) + notification_from_db = Notification.query.get(sample_notification.id) + assert notification_from_db.status == 'failed' + assert notification_from_db._status_enum == 'failed' + assert notification_from_db._status_fkey == 'failed' + + @freeze_time("2016-01-10 12:00:00.000000") def test_should_delete_notifications_after_seven_days(notify_db, notify_db_session): assert len(Notification.query.all()) == 0 diff --git a/tests/app/service/test_rest.py b/tests/app/service/test_rest.py index 19f42c880..2e6fa9289 100644 --- a/tests/app/service/test_rest.py +++ b/tests/app/service/test_rest.py @@ -1240,17 +1240,16 @@ def test_get_monthly_notification_stats(mocker, client, sample_service, url, exp assert json.loads(response.get_data(as_text=True)) == expected_json -def test_get_services_with_detailed_flag(notify_api, notify_db, notify_db_session): +def test_get_services_with_detailed_flag(client, notify_db, notify_db_session): notifications = [ create_sample_notification(notify_db, notify_db_session), create_sample_notification(notify_db, notify_db_session), create_sample_notification(notify_db, notify_db_session, key_type=KEY_TYPE_TEST) ] - with notify_api.test_request_context(), notify_api.test_client() as client: - resp = client.get( - '/service?detailed=True', - headers=[create_authorization_header()] - ) + resp = client.get( + '/service?detailed=True', + headers=[create_authorization_header()] + ) assert resp.status_code == 200 data = json.loads(resp.get_data(as_text=True))['data'] diff --git a/tests/conftest.py b/tests/conftest.py index b08059725..61cd17ce9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -76,7 +76,8 @@ def notify_db_session(notify_db): "job_status", "provider_details_history", "template_process_type", - "dvla_organisation"]: + "dvla_organisation", + "notification_status_types"]: notify_db.engine.execute(tbl.delete()) notify_db.session.commit() From d252dc89762b7fcef2596fd68187dcc16cd7b0bd Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Fri, 5 May 2017 14:12:50 +0100 Subject: [PATCH 04/15] New endpoint to search by "to" field of the notification. The query ignores case and spaces. --- app/dao/notifications_dao.py | 7 ++++ app/service/rest.py | 7 ++++ tests/app/dao/test_notification_dao.py | 32 +++++++++++++++++- tests/app/service/test_rest.py | 45 ++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 1 deletion(-) diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index aefc7a1fd..da81c348b 100644 --- a/app/dao/notifications_dao.py +++ b/app/dao/notifications_dao.py @@ -456,3 +456,10 @@ def dao_update_notifications_sent_to_dvla(job_id, provider): {'status': NOTIFICATION_SENDING, "sent_by": provider, "sent_at": now, "updated_at": now}) return updated_count + + +@statsd(namespace="dao") +def dao_get_notifications_by_to_field(service_id, search_term): + return Notification.query.filter( + Notification.service_id == service_id, + func.replace(func.lower(Notification.to), " ", "") == search_term.lower().replace(" ", "")).all() diff --git a/app/service/rest.py b/app/service/rest.py index 25991f30d..01a7fd07a 100644 --- a/app/service/rest.py +++ b/app/service/rest.py @@ -271,6 +271,13 @@ def get_all_notifications_for_service(service_id): ), 200 +@service_blueprint.route('//notification/', methods=['GET']) +def search_for_notification_by_to_field(service_id, search_term): + results = notifications_dao.dao_get_notifications_by_to_field(service_id, search_term) + return jsonify( + notifications=notification_with_template_schema.dump(results, many=True).data), 200 + + @service_blueprint.route('//notifications/monthly', methods=['GET']) def get_monthly_notification_stats(service_id): service = dao_fetch_service_by_id(service_id) diff --git a/tests/app/dao/test_notification_dao.py b/tests/app/dao/test_notification_dao.py index b5461e036..52ca33329 100644 --- a/tests/app/dao/test_notification_dao.py +++ b/tests/app/dao/test_notification_dao.py @@ -39,7 +39,7 @@ from app.dao.notifications_dao import ( dao_delete_notifications_and_history_by_id, dao_timeout_notifications, is_delivery_slow_for_provider, - dao_update_notifications_sent_to_dvla) + dao_update_notifications_sent_to_dvla, dao_get_notifications_by_to_field) from app.dao.services_dao import dao_update_service from tests.app.db import create_notification @@ -1626,3 +1626,33 @@ def test_dao_update_notifications_sent_to_dvla_does_update_history_if_test_key( assert updated_notification.sent_at assert updated_notification.updated_at assert not NotificationHistory.query.get(notification.id) + + +def test_dao_get_notifications_by_to_field(sample_template): + notification1 = create_notification(template=sample_template, to_field='+447700900855') + notification2 = create_notification(template=sample_template, to_field='jack@gmail.com') + notification3 = create_notification(template=sample_template, to_field='jane@gmail.com') + results = dao_get_notifications_by_to_field(notification1.service_id, "+447700900855") + assert len(results) == 1 + assert results[0].id == notification1.id + + +def test_dao_get_notifications_by_to_field_search_is_not_case_sensitive(sample_template): + notification1 = create_notification(template=sample_template, to_field='+447700900855') + notification2 = create_notification(template=sample_template, to_field='jack@gmail.com') + notification3 = create_notification(template=sample_template, to_field='jane@gmail.com') + results = dao_get_notifications_by_to_field(notification1.service_id, 'JACK@gmail.com') + assert len(results) == 1 + assert results[0].id == notification2.id + + +def test_dao_get_notifications_by_to_field_search_ignores_spaces(sample_template): + notification1 = create_notification(template=sample_template, to_field='+447700900855') + notification2 = create_notification(template=sample_template, to_field='+44 77 00900 855') + notification3 = create_notification(template=sample_template, to_field=' +4477009 00 855 ') + notification4 = create_notification(template=sample_template, to_field='jack@gmail.com') + results = dao_get_notifications_by_to_field(notification1.service_id, '+447700900855') + assert len(results) == 3 + assert notification1.id in [r.id for r in results] + assert notification2.id in [r.id for r in results] + assert notification3.id in [r.id for r in results] diff --git a/tests/app/service/test_rest.py b/tests/app/service/test_rest.py index 19f42c880..22e1dfd3f 100644 --- a/tests/app/service/test_rest.py +++ b/tests/app/service/test_rest.py @@ -1614,3 +1614,48 @@ def test_get_monthly_billing_usage_returns_empty_list_if_no_notifications(client ) assert response.status_code == 200 assert json.loads(response.get_data(as_text=True)) == [] + + +def test_search_for_notification_by_to_field(client, notify_db, notify_db_session): + notification1 = create_sample_notification(notify_db, notify_db_session, + to_field="+447700900855") + notification2 = create_sample_notification(notify_db, notify_db_session, to_field="jack@gmail.com") + + response = client.get('/service/{}/notification/{}'.format(notification1.service_id, "jack@gmail.com"), + headers=[create_authorization_header()]) + assert response.status_code == 200 + result = json.loads(response.get_data(as_text=True)) + assert len(result["notifications"]) == 1 + assert result["notifications"][0]["id"] == str(notification2.id) + + +def test_search_for_notification_by_to_field_return_empty_list_if_there_is_no_match( + client, notify_db, notify_db_session): + notification1 = create_sample_notification(notify_db, notify_db_session, + to_field="+447700900855") + notification2 = create_sample_notification(notify_db, notify_db_session, to_field="jack@gmail.com") + + response = client.get('/service/{}/notification/{}'.format(notification1.service_id, "+447700900800"), + headers=[create_authorization_header()]) + assert response.status_code == 200 + assert len(json.loads(response.get_data(as_text=True))["notifications"]) == 0 + + +def test_search_for_notification_by_to_field_return_multiple_matches( + client, notify_db, notify_db_session): + notification1 = create_sample_notification(notify_db, notify_db_session, + to_field="+447700900855") + notification2 = create_sample_notification(notify_db, notify_db_session, + to_field=" +44 77009 00855 ") + notification3 = create_sample_notification(notify_db, notify_db_session, + to_field="+44770 0900 855") + notification4 = create_sample_notification(notify_db, notify_db_session, to_field="jack@gmail.com") + + response = client.get('/service/{}/notification/{}'.format(notification1.service_id, "+447700900855"), + headers=[create_authorization_header()]) + assert response.status_code == 200 + result = json.loads(response.get_data(as_text=True)) + assert len(result["notifications"]) == 3 + assert str(notification1.id) in [n["id"] for n in result["notifications"]] + assert str(notification2.id) in [n["id"] for n in result["notifications"]] + assert str(notification3.id) in [n["id"] for n in result["notifications"]] From 35136704086b5ffa43e5b9d2b2ac88b85f43c167 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Mon, 8 May 2017 16:40:56 +0100 Subject: [PATCH 05/15] Script to update the international flag in the notifications and notification_history tables. This script may move where it is not run on db migration but as a one-off script from a server command. --- .../set_notifications_international.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 migrations/versions/set_notifications_international.py diff --git a/migrations/versions/set_notifications_international.py b/migrations/versions/set_notifications_international.py new file mode 100644 index 000000000..e476fae4e --- /dev/null +++ b/migrations/versions/set_notifications_international.py @@ -0,0 +1,37 @@ +"""empty message + +Revision ID: set_notifications_international +Revises: 0080_fix_rate_start_date +Create Date: 2017-05-05 15:26:34.621670 + +""" + +# revision identifiers, used by Alembic. +from datetime import datetime + +revision = 'set_notifications_international' +down_revision = '0080_fix_rate_start_date' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + conn = op.get_bind() + start = datetime.utcnow() + all_notifications = "select id from notifications where international is null limit 1000" + + results = conn.execute(all_notifications) + res = results.fetchall() + while len(res) > 0: + conn.execute("update notifications set international = False where id in ({})".format(all_notifications)) + conn.execute("update notification_history set international = False where id in ({})".format(all_notifications)) + conn.commit() + results = conn.execute(all_notifications) + res = results.fetchall() + end = datetime.utcnow() + print("Started at: {} ended at: {}".format(start, end)) + +def downgrade(): + # There is no way to downgrade this update. + pass \ No newline at end of file From a564b9aeb010cb654547e3d3c94cb25b64990f3c Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Mon, 8 May 2017 17:20:21 +0100 Subject: [PATCH 06/15] Update /service/{}/notifications to look for a "to" query string, if it exists call the search notifications_by_to_field method. --- app/schemas.py | 1 + app/service/rest.py | 4 +++- tests/app/service/test_rest.py | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/schemas.py b/app/schemas.py index 2a42c7d15..2435b9a83 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -492,6 +492,7 @@ class NotificationsFilterSchema(ma.Schema): include_from_test_key = fields.Boolean(required=False) older_than = fields.UUID(required=False) format_for_csv = fields.String() + to = fields.String() @pre_load def handle_multidict(self, in_data): diff --git a/app/service/rest.py b/app/service/rest.py index 01a7fd07a..4122db82a 100644 --- a/app/service/rest.py +++ b/app/service/rest.py @@ -242,6 +242,8 @@ def get_service_history(service_id): @service_blueprint.route('//notifications', methods=['GET']) def get_all_notifications_for_service(service_id): data = notifications_filter_schema.load(request.args).data + if data.get("to", None): + return search_for_notification_by_to_field(service_id, request.query_string.decode()) page = data['page'] if 'page' in data else 1 page_size = data['page_size'] if 'page_size' in data else current_app.config.get('PAGE_SIZE') limit_days = data.get('limit_days') @@ -271,8 +273,8 @@ def get_all_notifications_for_service(service_id): ), 200 -@service_blueprint.route('//notification/', methods=['GET']) def search_for_notification_by_to_field(service_id, search_term): + search_term = search_term.replace('to=', '') results = notifications_dao.dao_get_notifications_by_to_field(service_id, search_term) return jsonify( notifications=notification_with_template_schema.dump(results, many=True).data), 200 diff --git a/tests/app/service/test_rest.py b/tests/app/service/test_rest.py index 22e1dfd3f..f0bae91cb 100644 --- a/tests/app/service/test_rest.py +++ b/tests/app/service/test_rest.py @@ -1621,7 +1621,7 @@ def test_search_for_notification_by_to_field(client, notify_db, notify_db_sessio to_field="+447700900855") notification2 = create_sample_notification(notify_db, notify_db_session, to_field="jack@gmail.com") - response = client.get('/service/{}/notification/{}'.format(notification1.service_id, "jack@gmail.com"), + response = client.get('/service/{}/notifications?to={}'.format(notification1.service_id, "jack@gmail.com"), headers=[create_authorization_header()]) assert response.status_code == 200 result = json.loads(response.get_data(as_text=True)) @@ -1635,7 +1635,7 @@ def test_search_for_notification_by_to_field_return_empty_list_if_there_is_no_ma to_field="+447700900855") notification2 = create_sample_notification(notify_db, notify_db_session, to_field="jack@gmail.com") - response = client.get('/service/{}/notification/{}'.format(notification1.service_id, "+447700900800"), + response = client.get('/service/{}/notifications?to={}'.format(notification1.service_id, "+447700900800"), headers=[create_authorization_header()]) assert response.status_code == 200 assert len(json.loads(response.get_data(as_text=True))["notifications"]) == 0 @@ -1651,7 +1651,7 @@ def test_search_for_notification_by_to_field_return_multiple_matches( to_field="+44770 0900 855") notification4 = create_sample_notification(notify_db, notify_db_session, to_field="jack@gmail.com") - response = client.get('/service/{}/notification/{}'.format(notification1.service_id, "+447700900855"), + response = client.get('/service/{}/notifications?to={}'.format(notification1.service_id, "+447700900855"), headers=[create_authorization_header()]) assert response.status_code == 200 result = json.loads(response.get_data(as_text=True)) From 4ff4717fdfa2699e6b362c97131f07e0ef8eebc2 Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Tue, 9 May 2017 11:09:16 +0100 Subject: [PATCH 07/15] make sure we update both statuses when updating notification history --- app/models.py | 4 ++++ tests/app/dao/test_notification_dao.py | 27 +++++++++++++++++++------- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/app/models.py b/app/models.py index 605b7bab8..1b07a6fc5 100644 --- a/app/models.py +++ b/app/models.py @@ -895,6 +895,10 @@ class NotificationHistory(db.Model, HistoryModel): history.status = notification.status return history + def update_from_original(self, original): + super().update_from_original(original) + self.status = original.status + @hybrid_property def status(self): return self._status_enum diff --git a/tests/app/dao/test_notification_dao.py b/tests/app/dao/test_notification_dao.py index 9c5006de7..68618a272 100644 --- a/tests/app/dao/test_notification_dao.py +++ b/tests/app/dao/test_notification_dao.py @@ -839,22 +839,35 @@ def test_update_notification(sample_notification): def test_update_notification_with_no_notification_status(sample_notification): # specifically, it has an old enum status, but not a new status (because the upgrade script has just run) - sample_notification._status_fkey = None - sample_notification._enum_status = 'created' - dao_update_notification(sample_notification) - - assert sample_notification.status == 'created' - assert sample_notification._enum_status == 'created' - assert sample_notification._status_fkey == None + update_dict = {'_status_enum': 'created', '_status_fkey': None} + Notification.query.filter(Notification.id == sample_notification.id).update(update_dict) + # now lets update the status to failed - both columns should now be populated sample_notification.status = 'failed' dao_update_notification(sample_notification) + notification_from_db = Notification.query.get(sample_notification.id) assert notification_from_db.status == 'failed' assert notification_from_db._status_enum == 'failed' assert notification_from_db._status_fkey == 'failed' +def test_updating_notification_with_no_notification_status_updates_notification_history(sample_notification): + # same as above, but with notification history + update_dict = {'_status_enum': 'created', '_status_fkey': None} + Notification.query.filter(Notification.id == sample_notification.id).update(update_dict) + NotificationHistory.query.filter(NotificationHistory.id == sample_notification.id).update(update_dict) + + # now lets update the notification's status to failed - both columns should now be populated on the history object + sample_notification.status = 'failed' + dao_update_notification(sample_notification) + + hist_from_db = NotificationHistory.query.get(sample_notification.id) + assert hist_from_db.status == 'failed' + assert hist_from_db._status_enum == 'failed' + assert hist_from_db._status_fkey == 'failed' + + @freeze_time("2016-01-10 12:00:00.000000") def test_should_delete_notifications_after_seven_days(notify_db, notify_db_session): assert len(Notification.query.all()) == 0 From c10d4a36e7e4f37e5ea40f07fc788ed80f8f45dd Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Tue, 9 May 2017 15:46:58 +0100 Subject: [PATCH 08/15] populate notification_status_types table, and add foreign keys --- .../versions/0081_noti_status_as_enum.py | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/migrations/versions/0081_noti_status_as_enum.py b/migrations/versions/0081_noti_status_as_enum.py index 40dfd9f1e..a45006665 100644 --- a/migrations/versions/0081_noti_status_as_enum.py +++ b/migrations/versions/0081_noti_status_as_enum.py @@ -15,19 +15,49 @@ import sqlalchemy as sa def upgrade(): - op.create_table('notification_status', + status_table = op.create_table('notification_status_types', sa.Column('name', sa.String(), nullable=False), sa.PrimaryKeyConstraint('name') ) + op.bulk_insert(status_table, + [ + {'name': x} for x in { + 'created', + 'sending', + 'delivered', + 'pending', + 'failed', + 'technical-failure', + 'temporary-failure', + 'permanent-failure', + 'sent', + } + ] + ) op.execute('ALTER TABLE notifications ADD COLUMN notification_status text') op.execute('ALTER TABLE notification_history ADD COLUMN notification_status text') op.create_index(op.f('ix_notifications_notification_status'), 'notifications', ['notification_status']) op.create_index(op.f('ix_notification_history_notification_status'), 'notification_history', ['notification_status']) + op.create_foreign_key( + 'fk_notifications_notification_status', + 'notifications', + 'notification_status_types', + ['notification_status'], + ['name'], + ) + op.create_foreign_key( + 'fk_notification_history_notification_status', + 'notification_history', + 'notification_status_types', + ['notification_status'], + ['name'], + ) + def downgrade(): op.execute('ALTER TABLE notifications DROP COLUMN notification_status') op.execute('ALTER TABLE notification_history DROP COLUMN notification_status') - op.execute('DROP TABLE notification_status') + op.execute('DROP TABLE notification_status_types') From 5ec4829d00bdab01a0a7d2849b43dc7adb42e53d Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Wed, 10 May 2017 11:04:12 +0100 Subject: [PATCH 09/15] fix v2 schema phone/email validation when non-str passed in jsonschema states: > A format attribute can generally only validate a given set of > instance types. If the type of the instance to validate is not in > this set, validation for this format attribute and instance SHOULD > succeed. We were not checking for the type of the input, and our validators were behaving in unexpected manners (throwing TypeErrors etc etc). Despite us declaring that the phone_number field is of type `str`, we still need to make sure the validator passes gracefully, so that the inbuilt type check can be the bit that catches if someone passes in a non-str value. We've seen this with people passing in integers instead of strs for phone numbers. This'll make them receive a nice 400 error (e.g. "phone_number 12345 is not of type string"), rather than us having a 500 internal server error --- app/schema_validation/__init__.py | 4 +-- .../test_notification_schemas.py | 31 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/schema_validation/__init__.py b/app/schema_validation/__init__.py index 72767c881..376315a8a 100644 --- a/app/schema_validation/__init__.py +++ b/app/schema_validation/__init__.py @@ -10,13 +10,13 @@ def validate(json_to_validate, schema): @format_checker.checks('phone_number', raises=InvalidPhoneError) def validate_schema_phone_number(instance): - if instance is not None: + if isinstance(instance, str): validate_phone_number(instance, international=True) return True @format_checker.checks('email_address', raises=InvalidEmailError) def validate_schema_email_address(instance): - if instance is not None: + if isinstance(instance, str): validate_email_address(instance) return True diff --git a/tests/app/v2/notifications/test_notification_schemas.py b/tests/app/v2/notifications/test_notification_schemas.py index 04c32e1c7..65a03b84d 100644 --- a/tests/app/v2/notifications/test_notification_schemas.py +++ b/tests/app/v2/notifications/test_notification_schemas.py @@ -131,10 +131,15 @@ def test_post_sms_schema_with_personalisation_that_is_not_a_dict(): assert len(error.keys()) == 2 -@pytest.mark.parametrize('invalid_phone_number, err_msg', - [('08515111111', 'phone_number Not a UK mobile number'), - ('07515111*11', 'phone_number Must not contain letters or symbols'), - ('notaphoneumber', 'phone_number Must not contain letters or symbols')]) +@pytest.mark.parametrize('invalid_phone_number, err_msg', [ + ('08515111111', 'phone_number Not a UK mobile number'), + ('07515111*11', 'phone_number Must not contain letters or symbols'), + ('notaphoneumber', 'phone_number Must not contain letters or symbols'), + (7700900001, 'phone_number 7700900001 is not of type string'), + (None, 'phone_number None is not of type string'), + ([], 'phone_number [] is not of type string'), + ({}, 'phone_number {} is not of type string'), +]) def test_post_sms_request_schema_invalid_phone_number(invalid_phone_number, err_msg): j = {"phone_number": invalid_phone_number, "template_id": str(uuid.uuid4()) @@ -213,12 +218,22 @@ def test_post_email_schema_bad_uuid_and_missing_email_address(): validate(j, post_email_request_schema) -def test_post_email_schema_invalid_email_address(): - j = {"template_id": str(uuid.uuid4()), - "email_address": "notavalidemail@address"} - with pytest.raises(ValidationError): +@pytest.mark.parametrize('email_address, err_msg', [ + ('example', 'email_address Not a valid email address'), + (12345, 'email_address 12345 is not of type string'), + (None, 'email_address None is not of type string'), + ([], 'email_address [] is not of type string'), + ({}, 'email_address {} is not of type string'), +]) +def test_post_email_schema_invalid_email_address(email_address, err_msg): + j = {"template_id": str(uuid.uuid4()), "email_address": email_address} + with pytest.raises(ValidationError) as e: validate(j, post_email_request_schema) + errors = json.loads(str(e.value)).get('errors') + assert len(errors) == 1 + assert {"error": "ValidationError", "message": err_msg} == errors[0] + def valid_email_response(): return { From f0a5851d73b76f3a59e2c15d7498ab074239eeaa Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Wed, 10 May 2017 14:02:11 +0100 Subject: [PATCH 10/15] The script updates 10,000 rows at a time, committing after each one. This should limit the distrubtion to the environment as only the rows being updated will be locked. It is likely those rows will not have a conflicting update at the same time since the records are older than 3 days. --- ...ons_international.py => 0082_set_international.py} | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) rename migrations/versions/{set_notifications_international.py => 0082_set_international.py} (69%) diff --git a/migrations/versions/set_notifications_international.py b/migrations/versions/0082_set_international.py similarity index 69% rename from migrations/versions/set_notifications_international.py rename to migrations/versions/0082_set_international.py index e476fae4e..97d3f5881 100644 --- a/migrations/versions/set_notifications_international.py +++ b/migrations/versions/0082_set_international.py @@ -1,6 +1,6 @@ """empty message -Revision ID: set_notifications_international +Revision ID: 0082_set_international Revises: 0080_fix_rate_start_date Create Date: 2017-05-05 15:26:34.621670 @@ -9,7 +9,7 @@ Create Date: 2017-05-05 15:26:34.621670 # revision identifiers, used by Alembic. from datetime import datetime -revision = 'set_notifications_international' +revision = '0082_set_international' down_revision = '0080_fix_rate_start_date' from alembic import op @@ -19,14 +19,17 @@ import sqlalchemy as sa def upgrade(): conn = op.get_bind() start = datetime.utcnow() - all_notifications = "select id from notifications where international is null limit 1000" + all_notifications = "select id from notification_history where international is null limit 10000" results = conn.execute(all_notifications) res = results.fetchall() + + conn.execute("update notifications set international = False where id in ({})".format(all_notifications)) + conn.execute("update notification_history set international = False where id in ({})".format(all_notifications)) + while len(res) > 0: conn.execute("update notifications set international = False where id in ({})".format(all_notifications)) conn.execute("update notification_history set international = False where id in ({})".format(all_notifications)) - conn.commit() results = conn.execute(all_notifications) res = results.fetchall() end = datetime.utcnow() From d1e9586fbbadd8278d1d4023490df3348915b217 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Wed, 10 May 2017 14:38:21 +0100 Subject: [PATCH 11/15] Update the script to set the international flag to do the notifications and notification_history in separate loops. It takes about 1.5 minutes to update 27,000 notifications and 27,000 notification_history. The update is a row level lock so will only affect updates to the same row. This is unlikely as the data being updated should be older than 3 days. The second scripts updates the table to set international as not null, to make the model. --- migrations/versions/0082_set_international.py | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/migrations/versions/0082_set_international.py b/migrations/versions/0082_set_international.py index 97d3f5881..fa89bd2f7 100644 --- a/migrations/versions/0082_set_international.py +++ b/migrations/versions/0082_set_international.py @@ -1,40 +1,45 @@ """empty message Revision ID: 0082_set_international -Revises: 0080_fix_rate_start_date +Revises: 0081_noti_status_as_enum Create Date: 2017-05-05 15:26:34.621670 """ +from datetime import datetime +from alembic import op # revision identifiers, used by Alembic. -from datetime import datetime - revision = '0082_set_international' -down_revision = '0080_fix_rate_start_date' - -from alembic import op -import sqlalchemy as sa +down_revision = '0081_noti_status_as_enum' def upgrade(): conn = op.get_bind() start = datetime.utcnow() - all_notifications = "select id from notification_history where international is null limit 10000" + notification_history = "select id from notification_history where international is null limit 10000" - results = conn.execute(all_notifications) + results = conn.execute(notification_history) res = results.fetchall() - conn.execute("update notifications set international = False where id in ({})".format(all_notifications)) - conn.execute("update notification_history set international = False where id in ({})".format(all_notifications)) - while len(res) > 0: - conn.execute("update notifications set international = False where id in ({})".format(all_notifications)) - conn.execute("update notification_history set international = False where id in ({})".format(all_notifications)) - results = conn.execute(all_notifications) + conn.execute("update notification_history set international = False where id in ({})".format( + notification_history)) + results = conn.execute(notification_history) res = results.fetchall() + + notifications = "select id from notifications where international is null limit 10000" + results2 = conn.execute(notifications) + res2 = results2.fetchall() + while len(res2) > 0: + conn.execute("update notifications set international = False where id in ({})".format(notifications)) + + results2 = conn.execute(notifications) + res2 = results2.fetchall() + end = datetime.utcnow() print("Started at: {} ended at: {}".format(start, end)) + def downgrade(): # There is no way to downgrade this update. - pass \ No newline at end of file + pass From ed4a8fba6c80be7d4611cfbafef158adb56e2122 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Wed, 10 May 2017 16:14:30 +0100 Subject: [PATCH 12/15] Script to update the international column to be not null --- .../0083_set_international_not_null.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 migrations/versions/0083_set_international_not_null.py diff --git a/migrations/versions/0083_set_international_not_null.py b/migrations/versions/0083_set_international_not_null.py new file mode 100644 index 000000000..da3a7491b --- /dev/null +++ b/migrations/versions/0083_set_international_not_null.py @@ -0,0 +1,31 @@ +"""empty message + +Revision ID: 0083_set_international_not_null +Revises: 0082_set_international +Create Date: 2017-05-10 14:08:51.067762 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '0083_set_international_not_null' +down_revision = '0082_set_international' + + +def upgrade(): + op.alter_column('notification_history', 'international', + existing_type=sa.BOOLEAN(), + nullable=False) + op.alter_column('notifications', 'international', + existing_type=sa.BOOLEAN(), + nullable=False) + + +def downgrade(): + op.alter_column('notifications', 'international', + existing_type=sa.BOOLEAN(), + nullable=True) + op.alter_column('notification_history', 'international', + existing_type=sa.BOOLEAN(), + nullable=True) From 466c833c39e143146448719da9ee63485c3d7a77 Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Wed, 10 May 2017 17:30:09 +0100 Subject: [PATCH 13/15] marshmallow schemas no longer return _status_enum column now return `status`, as they should --- app/schemas.py | 8 ++++++-- tests/app/job/test_rest.py | 19 +++++++++++++++++++ tests/app/test_schemas.py | 17 ++++++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/app/schemas.py b/app/schemas.py index 2435b9a83..aa8980865 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -220,7 +220,9 @@ class NotificationModelSchema(BaseSchema): class Meta: model = models.Notification strict = True - exclude = ('_personalisation', 'job', 'service', 'template', 'api_key', '') + exclude = ('_personalisation', 'job', 'service', 'template', 'api_key', '_status_enum', '_status_fkey') + + status = fields.String(required=False) class BaseTemplateSchema(BaseSchema): @@ -315,6 +317,7 @@ class NotificationSchema(ma.Schema): class Meta: strict = True + status = fields.String(required=False) personalisation = fields.Dict(required=False) @@ -369,7 +372,7 @@ class NotificationWithTemplateSchema(BaseSchema): class Meta: model = models.Notification strict = True - exclude = ('_personalisation',) + exclude = ('_personalisation', '_status_enum', '_status_fkey') template = fields.Nested( TemplateSchema, @@ -377,6 +380,7 @@ class NotificationWithTemplateSchema(BaseSchema): dump_only=True ) job = fields.Nested(JobSchema, only=["id", "original_file_name"], dump_only=True) + status = fields.String(required=False) personalisation = fields.Dict(required=False) key_type = field_for(models.Notification, 'key_type', required=True) key_name = fields.String() diff --git a/tests/app/job/test_rest.py b/tests/app/job/test_rest.py index e05d52b3a..d583d6b31 100644 --- a/tests/app/job/test_rest.py +++ b/tests/app/job/test_rest.py @@ -447,6 +447,25 @@ def test_get_all_notifications_for_job_filtered_by_status( assert response.status_code == 200 +def test_get_all_notifications_for_job_returns_correct_format( + client, + sample_notification_with_job +): + service_id = sample_notification_with_job.service_id + job_id = sample_notification_with_job.job_id + response = client.get( + path='/service/{}/job/{}/notifications'.format(service_id, job_id), + headers=[create_authorization_header()] + ) + assert response.status_code == 200 + resp = json.loads(response.get_data(as_text=True)) + assert len(resp['notifications']) == 1 + assert resp['notifications'][0]['id'] == str(sample_notification_with_job.id) + assert resp['notifications'][0]['status'] == sample_notification_with_job.status + assert '_status_fkey' not in resp['notifications'][0] + assert '_status_enum' not in resp['notifications'][0] + + def test_get_job_by_id(notify_api, sample_job): job_id = str(sample_job.id) service_id = sample_job.service.id diff --git a/tests/app/test_schemas.py b/tests/app/test_schemas.py index 4d435e09e..4ab15f569 100644 --- a/tests/app/test_schemas.py +++ b/tests/app/test_schemas.py @@ -1,5 +1,4 @@ import pytest - from marshmallow import ValidationError from sqlalchemy import desc @@ -33,6 +32,22 @@ def test_notification_schema_adds_api_key_name(sample_notification_with_api_key) assert data['key_name'] == 'Test key' +@pytest.mark.parametrize('schema_name', [ + 'notification_with_template_schema', + 'notification_schema', + 'notification_with_template_schema', + 'notification_with_personalisation_schema', +]) +def test_notification_schema_has_correct_status(sample_notification, schema_name): + from app import schemas + + data = getattr(schemas, schema_name).dump(sample_notification).data + + assert data['status'] == sample_notification.status + assert '_status_enum' not in data + assert '_status_fkey' not in data + + @pytest.mark.parametrize('user_attribute, user_value', [ ('name', 'New User'), ('email_address', 'newuser@mail.com'), From c7479b6fbbf9ac8e58bfd88a2421c231b25d1b12 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Thu, 11 May 2017 12:12:27 +0100 Subject: [PATCH 14/15] Revert "Update notification international" --- migrations/versions/0082_set_international.py | 45 ------------------- .../0083_set_international_not_null.py | 31 ------------- 2 files changed, 76 deletions(-) delete mode 100644 migrations/versions/0082_set_international.py delete mode 100644 migrations/versions/0083_set_international_not_null.py diff --git a/migrations/versions/0082_set_international.py b/migrations/versions/0082_set_international.py deleted file mode 100644 index fa89bd2f7..000000000 --- a/migrations/versions/0082_set_international.py +++ /dev/null @@ -1,45 +0,0 @@ -"""empty message - -Revision ID: 0082_set_international -Revises: 0081_noti_status_as_enum -Create Date: 2017-05-05 15:26:34.621670 - -""" -from datetime import datetime -from alembic import op - -# revision identifiers, used by Alembic. -revision = '0082_set_international' -down_revision = '0081_noti_status_as_enum' - - -def upgrade(): - conn = op.get_bind() - start = datetime.utcnow() - notification_history = "select id from notification_history where international is null limit 10000" - - results = conn.execute(notification_history) - res = results.fetchall() - - while len(res) > 0: - conn.execute("update notification_history set international = False where id in ({})".format( - notification_history)) - results = conn.execute(notification_history) - res = results.fetchall() - - notifications = "select id from notifications where international is null limit 10000" - results2 = conn.execute(notifications) - res2 = results2.fetchall() - while len(res2) > 0: - conn.execute("update notifications set international = False where id in ({})".format(notifications)) - - results2 = conn.execute(notifications) - res2 = results2.fetchall() - - end = datetime.utcnow() - print("Started at: {} ended at: {}".format(start, end)) - - -def downgrade(): - # There is no way to downgrade this update. - pass diff --git a/migrations/versions/0083_set_international_not_null.py b/migrations/versions/0083_set_international_not_null.py deleted file mode 100644 index da3a7491b..000000000 --- a/migrations/versions/0083_set_international_not_null.py +++ /dev/null @@ -1,31 +0,0 @@ -"""empty message - -Revision ID: 0083_set_international_not_null -Revises: 0082_set_international -Create Date: 2017-05-10 14:08:51.067762 - -""" -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision = '0083_set_international_not_null' -down_revision = '0082_set_international' - - -def upgrade(): - op.alter_column('notification_history', 'international', - existing_type=sa.BOOLEAN(), - nullable=False) - op.alter_column('notifications', 'international', - existing_type=sa.BOOLEAN(), - nullable=False) - - -def downgrade(): - op.alter_column('notifications', 'international', - existing_type=sa.BOOLEAN(), - nullable=True) - op.alter_column('notification_history', 'international', - existing_type=sa.BOOLEAN(), - nullable=True) From 373c2b029fa880814790185723f3e006c7fbcbf3 Mon Sep 17 00:00:00 2001 From: Imdad Ahad Date: Wed, 10 May 2017 17:22:24 +0100 Subject: [PATCH 15/15] Add go live template --- .../versions/0082_add_golive_template.py | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 migrations/versions/0082_add_golive_template.py diff --git a/migrations/versions/0082_add_golive_template.py b/migrations/versions/0082_add_golive_template.py new file mode 100644 index 000000000..faa6981b4 --- /dev/null +++ b/migrations/versions/0082_add_golive_template.py @@ -0,0 +1,119 @@ +"""empty message + +Revision ID: 0082_add_go_live_template +Revises: 0081_noti_status_as_enum +Create Date: 2017-05-10 16:06:04.070874 + +""" + +# revision identifiers, used by Alembic. +from datetime import datetime + +from flask import current_app + +from alembic import op +import sqlalchemy as sa + +revision = '0082_add_go_live_template' +down_revision = '0081_noti_status_as_enum' + +template_id = '618185c6-3636-49cd-b7d2-6f6f5eb3bdde' + + +def upgrade(): + template_insert = """ + INSERT INTO templates (id, name, template_type, created_at, content, archived, service_id, subject, created_by_id, version, process_type) + VALUES ('{}', '{}', '{}', '{}', '{}', False, '{}', '{}', '{}', 1, '{}') + """ + template_history_insert = """ + INSERT INTO templates_history (id, name, template_type, created_at, content, archived, service_id, subject, created_by_id, version, process_type) + VALUES ('{}', '{}', '{}', '{}', '{}', False, '{}', '{}', '{}', 1, '{}') + """ + + template_content = """Hi ((name)), + +((service name)) is now live on GOV.UK Notify. + +You can send up to ((message limit)) messages per day. + +As a live service, you’ll need to know who to contact if you have a question, or something goes wrong. + +^To get email updates whenever there is a problem with Notify, it’s important that you subscribe to our system status page: +https://status.notifications.service.gov.uk + +If our system status page shows a problem, then we’ve been alerted and are working on it – you don’t need to contact us. +#Problems or questions during office hours + +Our office hours are 9.30am to 5.30pm, Monday to Friday. + +To report a problem or ask a question, go to the support page: +https://www.notifications.service.gov.uk/support + +We’ll reply within 30 minutes whether you’re reporting a problem or just asking a question. + +The team are also available to answer questions on the cross-government Slack channel: +https://ukgovernmentdigital.slack.com/messages/govuk-notify + +#Problems or questions out of hours + +We offer out of hours support for emergencies. + +It’s only an emergency if: +* no one in your team can log in +* a ‘technical difficulties’ error appears when you try to upload a file +* a 500 response code appears when you try to send messages using the API + +If you have one of these emergencies, email details to: +ooh-gov-uk-notify-support@digital.cabinet-office.gov.uk + +^Only use this email address for out of hours emergencies. Don’t share this address with people outside of your team. + +We’ll get back to you within 30 minutes and give you hourly updates until the problem’s fixed. + +For non-emergency problems or questions, use our support page and we’ll reply in office hours: +https://www.notifications.service.gov.uk/support +#Escalation for emergency problems + +If we haven’t acknowledged an emergency problem you’ve reported within 30 minutes and you need to know what’s happening, you can escalate to: + +or + +Thanks +GOV.UK Notify team +""" + + template_name = "Automated \"You''re now live\" message" + template_subject = '((service name)) is now live on GOV.UK Notify' + + op.execute( + template_history_insert.format( + template_id, + template_name, + 'email', + datetime.utcnow(), + template_content, + current_app.config['NOTIFY_SERVICE_ID'], + template_subject, + current_app.config['NOTIFY_USER_ID'], + 'normal' + ) + ) + + op.execute( + template_insert.format( + template_id, + template_name, + 'email', + datetime.utcnow(), + template_content, + current_app.config['NOTIFY_SERVICE_ID'], + template_subject, + current_app.config['NOTIFY_USER_ID'], + 'normal' + ) + ) + + +def downgrade(): + op.execute("DELETE FROM templates_history WHERE id = '{}'".format(template_id)) + op.execute("DELETE FROM templates WHERE id = '{}'".format(template_id))