mirror of
https://github.com/GSA/notifications-api.git
synced 2026-06-15 02:38:37 -04:00
Merge pull request #526 from alphagov/notifications-shadow-table
Notification history table
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
from sqlalchemy import (desc, func, Integer, or_, and_, asc)
|
||||
from sqlalchemy.sql.expression import cast
|
||||
|
||||
import uuid
|
||||
from datetime import (
|
||||
datetime,
|
||||
timedelta,
|
||||
@@ -9,12 +7,16 @@ from datetime import (
|
||||
|
||||
from flask import current_app
|
||||
from werkzeug.datastructures import MultiDict
|
||||
from sqlalchemy import (desc, func, Integer, or_, and_, asc)
|
||||
from sqlalchemy.sql.expression import cast
|
||||
from notifications_utils.template import get_sms_fragment_count
|
||||
|
||||
from app import db
|
||||
from app.dao import days_ago
|
||||
from app.models import (
|
||||
Service,
|
||||
Notification,
|
||||
NotificationHistory,
|
||||
Job,
|
||||
NotificationStatistics,
|
||||
TemplateStatistics,
|
||||
@@ -23,15 +25,11 @@ from app.models import (
|
||||
Template,
|
||||
ProviderStatistics,
|
||||
ProviderDetails)
|
||||
|
||||
from notifications_utils.template import get_sms_fragment_count
|
||||
|
||||
from app.clients import (
|
||||
STATISTICS_FAILURE,
|
||||
STATISTICS_DELIVERED,
|
||||
STATISTICS_REQUESTED
|
||||
)
|
||||
|
||||
from app.dao.dao_utils import transactional
|
||||
|
||||
|
||||
@@ -182,7 +180,16 @@ def dao_create_notification(notification, notification_type):
|
||||
service_id=notification.service_id)
|
||||
db.session.add(template_stats)
|
||||
|
||||
if not notification.id:
|
||||
# need to populate defaulted fields before we create the notification history object
|
||||
notification.id = uuid.uuid4()
|
||||
if not notification.status:
|
||||
notification.status = 'created'
|
||||
|
||||
notification_history = NotificationHistory.from_notification(notification)
|
||||
|
||||
db.session.add(notification)
|
||||
db.session.add(notification_history)
|
||||
|
||||
|
||||
def _update_notification_stats_query(notification_type, status):
|
||||
@@ -240,7 +247,8 @@ def _update_notification_status(notification, status, notification_statistics_st
|
||||
if notification_statistics_status:
|
||||
_update_statistics(notification, notification_statistics_status)
|
||||
|
||||
db.session.query(Notification).filter(Notification.id == notification.id).update({Notification.status: status})
|
||||
notification.status = status
|
||||
dao_update_notification(notification)
|
||||
return True
|
||||
|
||||
|
||||
@@ -280,6 +288,8 @@ def update_notification_status_by_reference(reference, status, notification_stat
|
||||
|
||||
def dao_update_notification(notification):
|
||||
notification.updated_at = datetime.utcnow()
|
||||
notification_history = NotificationHistory.query.get(notification.id)
|
||||
notification_history.update_from_notification(notification)
|
||||
db.session.add(notification)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from app.models import (
|
||||
ApiKey,
|
||||
Template,
|
||||
Job,
|
||||
NotificationHistory,
|
||||
Notification,
|
||||
Permission,
|
||||
User,
|
||||
@@ -97,6 +98,7 @@ def delete_service_and_all_associated_db_objects(service):
|
||||
_delete_commit(Permission.query.filter_by(service=service))
|
||||
_delete_commit(ApiKey.query.filter_by(service=service))
|
||||
_delete_commit(ApiKey.get_history_model().query.filter_by(service_id=service.id))
|
||||
_delete_commit(NotificationHistory.query.filter_by(service=service))
|
||||
_delete_commit(Notification.query.filter_by(service=service))
|
||||
_delete_commit(Job.query.filter_by(service=service))
|
||||
_delete_commit(Template.query.filter_by(service=service))
|
||||
|
||||
@@ -332,6 +332,7 @@ class VerifyCode(db.Model):
|
||||
|
||||
NOTIFICATION_STATUS_TYPES = ['created', 'sending', 'delivered', 'pending', 'failed',
|
||||
'technical-failure', 'temporary-failure', 'permanent-failure']
|
||||
NOTIFICATION_STATUS_TYPES_ENUM = db.Enum(*NOTIFICATION_STATUS_TYPES, name='notify_status_type')
|
||||
|
||||
|
||||
class Notification(db.Model):
|
||||
@@ -370,8 +371,7 @@ class Notification(db.Model):
|
||||
unique=False,
|
||||
nullable=True,
|
||||
onupdate=datetime.datetime.utcnow)
|
||||
status = db.Column(
|
||||
db.Enum(*NOTIFICATION_STATUS_TYPES, name='notify_status_type'), nullable=False, default='created')
|
||||
status = db.Column(NOTIFICATION_STATUS_TYPES_ENUM, nullable=False, default='created')
|
||||
reference = db.Column(db.String, nullable=True, index=True)
|
||||
_personalisation = db.Column(db.String, nullable=True)
|
||||
|
||||
@@ -387,6 +387,39 @@ class Notification(db.Model):
|
||||
self._personalisation = encryption.encrypt(personalisation)
|
||||
|
||||
|
||||
class NotificationHistory(db.Model):
|
||||
__tablename__ = 'notification_history'
|
||||
|
||||
id = db.Column(UUID(as_uuid=True), primary_key=True)
|
||||
job_id = db.Column(UUID(as_uuid=True), db.ForeignKey('jobs.id'), index=True, unique=False)
|
||||
job = db.relationship('Job')
|
||||
job_row_number = db.Column(db.Integer, nullable=True)
|
||||
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, unique=False)
|
||||
service = db.relationship('Service')
|
||||
template_id = db.Column(UUID(as_uuid=True), db.ForeignKey('templates.id'), index=True, unique=False)
|
||||
template = db.relationship('Template')
|
||||
template_version = db.Column(db.Integer, nullable=False)
|
||||
api_key_id = db.Column(UUID(as_uuid=True), db.ForeignKey('api_keys.id'), index=True, unique=False)
|
||||
api_key = db.relationship('ApiKey')
|
||||
key_type = db.Column(db.String, db.ForeignKey('key_types.name'), index=True, unique=False, nullable=False)
|
||||
content_char_count = db.Column(db.Integer, nullable=True)
|
||||
notification_type = db.Column(notification_types, nullable=False)
|
||||
created_at = db.Column(db.DateTime, index=False, unique=False, nullable=False)
|
||||
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, nullable=False, default='created')
|
||||
reference = db.Column(db.String, nullable=True, index=True)
|
||||
|
||||
@classmethod
|
||||
def from_notification(cls, notification):
|
||||
return cls(**{c.name: getattr(notification, c.name) for c in cls.__table__.columns})
|
||||
|
||||
def update_from_notification(self, notification):
|
||||
for c in self.__table__.columns:
|
||||
setattr(self, c.name, getattr(notification, c.name))
|
||||
|
||||
|
||||
INVITED_USER_STATUS_TYPES = ['pending', 'accepted', 'cancelled']
|
||||
|
||||
|
||||
|
||||
101
migrations/versions/0042_notification_history.py
Normal file
101
migrations/versions/0042_notification_history.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 0042_notification_history
|
||||
Revises: 0041_email_template
|
||||
Create Date: 2016-07-07 13:15:35.503107
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0042_notification_history'
|
||||
down_revision = '0041_email_template'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('notification_history',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('job_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('job_row_number', sa.Integer(), nullable=True),
|
||||
sa.Column('service_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('template_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('template_version', sa.Integer(), nullable=False),
|
||||
sa.Column('api_key_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('key_type', sa.String(), nullable=False),
|
||||
sa.Column('content_char_count', sa.Integer(), nullable=True),
|
||||
sa.Column('notification_type', postgresql.ENUM('email', 'sms', 'letter', name='notification_type', create_type=False), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('sent_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('sent_by', sa.String(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('status', postgresql.ENUM('created', 'sending', 'delivered', 'pending', 'failed', 'technical-failure', 'temporary-failure', 'permanent-failure', name='notify_status_type', create_type=False), nullable=False),
|
||||
sa.Column('reference', sa.String(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['api_key_id'], ['api_keys.id'], ),
|
||||
sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], ),
|
||||
sa.ForeignKeyConstraint(['key_type'], ['key_types.name'], ),
|
||||
sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
|
||||
sa.ForeignKeyConstraint(['template_id'], ['templates.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_notification_history_api_key_id'), 'notification_history', ['api_key_id'], unique=False)
|
||||
op.create_index(op.f('ix_notification_history_job_id'), 'notification_history', ['job_id'], unique=False)
|
||||
op.create_index(op.f('ix_notification_history_key_type'), 'notification_history', ['key_type'], unique=False)
|
||||
op.create_index(op.f('ix_notification_history_reference'), 'notification_history', ['reference'], unique=False)
|
||||
op.create_index(op.f('ix_notification_history_service_id'), 'notification_history', ['service_id'], unique=False)
|
||||
op.create_index(op.f('ix_notification_history_template_id'), 'notification_history', ['template_id'], unique=False)
|
||||
|
||||
op.execute('''
|
||||
INSERT INTO notification_history
|
||||
(
|
||||
id,
|
||||
job_id,
|
||||
job_row_number,
|
||||
service_id,
|
||||
template_id,
|
||||
template_version,
|
||||
api_key_id,
|
||||
key_type,
|
||||
content_char_count,
|
||||
notification_type,
|
||||
created_at,
|
||||
sent_at,
|
||||
sent_by,
|
||||
updated_at,
|
||||
status,
|
||||
reference
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
job_id,
|
||||
job_row_number,
|
||||
service_id,
|
||||
template_id,
|
||||
template_version,
|
||||
api_key_id,
|
||||
key_type,
|
||||
content_char_count,
|
||||
notification_type,
|
||||
created_at,
|
||||
sent_at,
|
||||
sent_by,
|
||||
updated_at,
|
||||
status,
|
||||
reference
|
||||
FROM notifications
|
||||
''')
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_notification_history_template_id'), table_name='notification_history')
|
||||
op.drop_index(op.f('ix_notification_history_service_id'), table_name='notification_history')
|
||||
op.drop_index(op.f('ix_notification_history_reference'), table_name='notification_history')
|
||||
op.drop_index(op.f('ix_notification_history_key_type'), table_name='notification_history')
|
||||
op.drop_index(op.f('ix_notification_history_job_id'), table_name='notification_history')
|
||||
op.drop_index(op.f('ix_notification_history_api_key_id'), table_name='notification_history')
|
||||
op.drop_table('notification_history')
|
||||
### end Alembic commands ###
|
||||
@@ -11,6 +11,7 @@ from app import db
|
||||
|
||||
from app.models import (
|
||||
Notification,
|
||||
NotificationHistory,
|
||||
Job,
|
||||
NotificationStatistics,
|
||||
TemplateStatistics,
|
||||
@@ -57,14 +58,20 @@ def test_should_by_able_to_update_status_by_reference(sample_email_template, ses
|
||||
|
||||
|
||||
def test_should_by_able_to_update_status_by_id(sample_template, sample_job, mmg_provider):
|
||||
data = _notification_json(sample_template, job_id=sample_job.id, status='sending')
|
||||
notification = Notification(**data)
|
||||
dao_create_notification(notification, sample_template.template_type)
|
||||
with freeze_time('2000-01-01 12:00:00'):
|
||||
data = _notification_json(sample_template, job_id=sample_job.id, status='sending')
|
||||
notification = Notification(**data)
|
||||
dao_create_notification(notification, sample_template.template_type)
|
||||
|
||||
assert Notification.query.get(notification.id).status == 'sending'
|
||||
assert update_notification_status_by_id(notification.id, 'delivered', 'delivered')
|
||||
|
||||
with freeze_time('2000-01-02 12:00:00'):
|
||||
assert update_notification_status_by_id(notification.id, 'delivered', 'delivered')
|
||||
|
||||
assert Notification.query.get(notification.id).status == 'delivered'
|
||||
_assert_notification_stats(notification.service_id, sms_delivered=1, sms_requested=1, sms_failed=0)
|
||||
_assert_job_stats(notification.job_id, sent=1, count=1, delivered=1, failed=0)
|
||||
assert notification.updated_at == datetime(2000, 1, 2, 12, 0, 0)
|
||||
|
||||
|
||||
def test_should_not_update_status_by_id_if_not_sending_and_does_not_update_job(notify_db, notify_db_session):
|
||||
@@ -608,10 +615,10 @@ def test_save_notification_with_no_job(sample_template, mmg_provider):
|
||||
|
||||
|
||||
def test_get_notification(sample_notification):
|
||||
notifcation_from_db = get_notification(
|
||||
notification_from_db = get_notification(
|
||||
sample_notification.service.id,
|
||||
sample_notification.id)
|
||||
assert sample_notification == notifcation_from_db
|
||||
assert sample_notification == notification_from_db
|
||||
|
||||
|
||||
def test_save_notification_no_job_id(sample_template, mmg_provider):
|
||||
@@ -633,11 +640,11 @@ def test_save_notification_no_job_id(sample_template, mmg_provider):
|
||||
|
||||
|
||||
def test_get_notification_for_job(sample_notification):
|
||||
notifcation_from_db = get_notification_for_job(
|
||||
notification_from_db = get_notification_for_job(
|
||||
sample_notification.service.id,
|
||||
sample_notification.job_id,
|
||||
sample_notification.id)
|
||||
assert sample_notification == notifcation_from_db
|
||||
assert sample_notification == notification_from_db
|
||||
|
||||
|
||||
def test_get_all_notifications_for_job(notify_db, notify_db_session, sample_job):
|
||||
@@ -692,7 +699,7 @@ def test_should_delete_notifications_after_seven_days(notify_db, notify_db_sessi
|
||||
|
||||
# create one notification a day between 1st and 10th from 11:00 to 19:00
|
||||
for i in range(1, 11):
|
||||
past_date = '2016-01-{0:02d} {0:02d}:00:00.000000'.format(i, i)
|
||||
past_date = '2016-01-{0:02d} {0:02d}:00:00.000000'.format(i)
|
||||
with freeze_time(past_date):
|
||||
sample_notification(notify_db, notify_db_session, created_at=datetime.utcnow(), status="failed")
|
||||
|
||||
@@ -707,6 +714,22 @@ def test_should_delete_notifications_after_seven_days(notify_db, notify_db_sessi
|
||||
assert notification.created_at.date() >= date(2016, 1, 3)
|
||||
|
||||
|
||||
@freeze_time("2016-01-10 12:00:00.000000")
|
||||
def test_should_not_delete_notification_history(notify_db, notify_db_session):
|
||||
with freeze_time('2016-01-01 12:00'):
|
||||
notification = sample_notification(notify_db, notify_db_session, created_at=datetime.utcnow(), status="failed")
|
||||
notification_id = notification.id
|
||||
|
||||
assert Notification.query.count() == 1
|
||||
assert NotificationHistory.query.count() == 1
|
||||
|
||||
delete_notifications_created_more_than_a_week_ago('failed')
|
||||
|
||||
assert Notification.query.count() == 0
|
||||
assert NotificationHistory.query.count() == 1
|
||||
assert NotificationHistory.query.one().id == notification_id
|
||||
|
||||
|
||||
def test_should_not_delete_failed_notifications_before_seven_days(notify_db, notify_db_session):
|
||||
should_delete = datetime.utcnow() - timedelta(days=8)
|
||||
do_not_delete = datetime.utcnow() - timedelta(days=7)
|
||||
@@ -967,6 +990,35 @@ def test_sms_fragment_count(char_count, expected_sms_fragment_count):
|
||||
assert get_sms_fragment_count(char_count) == expected_sms_fragment_count
|
||||
|
||||
|
||||
def test_creating_notification_adds_to_notification_history(sample_template):
|
||||
data = _notification_json(sample_template)
|
||||
notification = Notification(**data)
|
||||
|
||||
dao_create_notification(notification, sample_template.template_type)
|
||||
|
||||
assert Notification.query.count() == 1
|
||||
|
||||
hist = NotificationHistory.query.one()
|
||||
assert hist.id == notification.id
|
||||
assert hist.created_at == notification.created_at
|
||||
assert hist.status == notification.status
|
||||
assert not hasattr(hist, 'to')
|
||||
assert not hasattr(hist, '_personalisation')
|
||||
|
||||
|
||||
def test_updating_notification_updates_notification_history(sample_notification):
|
||||
hist = NotificationHistory.query.one()
|
||||
assert hist.id == sample_notification.id
|
||||
assert hist.status == 'created'
|
||||
|
||||
sample_notification.status = 'sending'
|
||||
dao_update_notification(sample_notification)
|
||||
|
||||
hist = NotificationHistory.query.one()
|
||||
assert hist.id == sample_notification.id
|
||||
assert hist.status == 'sending'
|
||||
|
||||
|
||||
def _notification_json(sample_template, job_id=None, id=None, status=None):
|
||||
data = {
|
||||
'to': '+44709123456',
|
||||
|
||||
Reference in New Issue
Block a user