Merge pull request #2936 from alphagov/broadcast-event-model

add broadcast_event table
This commit is contained in:
Leo Hemsted
2020-07-28 15:03:20 +01:00
committed by GitHub
7 changed files with 230 additions and 19 deletions

View File

@@ -1,5 +1,5 @@
from app import db
from app.models import BroadcastMessage
from app.models import BroadcastMessage, BroadcastEvent
from app.dao.dao_utils import transactional
@@ -28,3 +28,17 @@ def dao_get_broadcast_messages_for_service(service_id):
return BroadcastMessage.query.filter(
BroadcastMessage.service_id == service_id
).order_by(BroadcastMessage.created_at)
def get_earlier_events_for_broadcast_event(broadcast_event_id):
"""
This is used to build up the references list.
"""
this_event = BroadcastEvent.query.get(broadcast_event_id)
return BroadcastEvent.query.filter(
BroadcastEvent.broadcast_message_id == this_event.broadcast_message_id,
BroadcastEvent.sent_at < this_event.sent_at
).order_by(
BroadcastEvent.sent_at.asc()
).all()

View File

@@ -39,6 +39,7 @@ from app import (
encryption,
DATETIME_FORMAT,
DATETIME_FORMAT_NO_TIMEZONE)
from app.utils import get_dt_string_or_none
from app.history_meta import Versioned
@@ -169,7 +170,7 @@ class User(db.Model):
'current_session_id': self.current_session_id,
'failed_login_count': self.failed_login_count,
'email_access_validated_at': self.email_access_validated_at.strftime(DATETIME_FORMAT),
'logged_in_at': self.logged_in_at.strftime(DATETIME_FORMAT) if self.logged_in_at else None,
'logged_in_at': get_dt_string_or_none(self.logged_in_at),
'mobile_number': self.mobile_number,
'organisations': [x.id for x in self.organisations if x.active],
'password_changed_at': self.password_changed_at.strftime(DATETIME_FORMAT_NO_TIMEZONE),
@@ -569,7 +570,7 @@ class AnnualBilling(db.Model):
'service_id': self.service_id,
'financial_year_start': self.financial_year_start,
"created_at": self.created_at.strftime(DATETIME_FORMAT),
"updated_at": self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None,
"updated_at": get_dt_string_or_none(self.updated_at),
"service": serialize_service() if self.service else None,
}
@@ -600,7 +601,7 @@ class InboundNumber(db.Model):
"service": serialize_service() if self.service else None,
"active": self.active,
"created_at": self.created_at.strftime(DATETIME_FORMAT),
"updated_at": self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None,
"updated_at": get_dt_string_or_none(self.updated_at),
}
@@ -631,7 +632,7 @@ class ServiceSmsSender(db.Model):
"archived": self.archived,
"inbound_number_id": str(self.inbound_number_id) if self.inbound_number_id else None,
"created_at": self.created_at.strftime(DATETIME_FORMAT),
"updated_at": self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None,
"updated_at": get_dt_string_or_none(self.updated_at),
}
@@ -723,7 +724,7 @@ class ServiceInboundApi(db.Model, Versioned):
"url": self.url,
"updated_by_id": str(self.updated_by_id),
"created_at": self.created_at.strftime(DATETIME_FORMAT),
"updated_at": self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None
"updated_at": get_dt_string_or_none(self.updated_at),
}
@@ -762,7 +763,7 @@ class ServiceCallbackApi(db.Model, Versioned):
"url": self.url,
"updated_by_id": str(self.updated_by_id),
"created_at": self.created_at.strftime(DATETIME_FORMAT),
"updated_at": self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None
"updated_at": get_dt_string_or_none(self.updated_at),
}
@@ -996,7 +997,7 @@ class TemplateBase(db.Model):
"id": str(self.id),
"type": self.template_type,
"created_at": self.created_at.strftime(DATETIME_FORMAT),
"updated_at": self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None,
"updated_at": get_dt_string_or_none(self.updated_at),
"created_by": self.created_by.email_address,
"version": self.version,
"body": self.content,
@@ -1628,7 +1629,7 @@ class Notification(db.Model):
"subject": self.subject,
"created_at": self.created_at.strftime(DATETIME_FORMAT),
"created_by_name": self.get_created_by_name(),
"sent_at": self.sent_at.strftime(DATETIME_FORMAT) if self.sent_at else None,
"sent_at": get_dt_string_or_none(self.sent_at),
"completed_at": self.completed_at(),
"scheduled_for": None,
"postage": self.postage
@@ -1955,7 +1956,7 @@ class ServiceEmailReplyTo(db.Model):
'is_default': self.is_default,
'archived': self.archived,
'created_at': self.created_at.strftime(DATETIME_FORMAT),
'updated_at': self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None
'updated_at': get_dt_string_or_none(self.updated_at),
}
@@ -1981,7 +1982,7 @@ class ServiceLetterContact(db.Model):
'is_default': self.is_default,
'archived': self.archived,
'created_at': self.created_at.strftime(DATETIME_FORMAT),
'updated_at': self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None
'updated_at': get_dt_string_or_none(self.updated_at),
}
@@ -2058,7 +2059,7 @@ class Complaint(db.Model):
'service_name': self.service.name,
'ses_feedback_id': str(self.ses_feedback_id),
'complaint_type': self.complaint_type,
'complaint_date': self.complaint_date.strftime(DATETIME_FORMAT) if self.complaint_date else None,
'complaint_date': get_dt_string_or_none(self.complaint_date),
'created_at': self.created_at.strftime(DATETIME_FORMAT),
}
@@ -2092,7 +2093,7 @@ class ServiceDataRetention(db.Model):
"notification_type": self.notification_type,
"days_of_retention": self.days_of_retention,
"created_at": self.created_at.strftime(DATETIME_FORMAT),
"updated_at": self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None,
"updated_at": get_dt_string_or_none(self.updated_at),
}
@@ -2266,15 +2267,92 @@ class BroadcastMessage(db.Model):
'status': self.status,
'starts_at': self.starts_at.strftime(DATETIME_FORMAT) if self.starts_at else None,
'finishes_at': self.finishes_at.strftime(DATETIME_FORMAT) if self.finishes_at else None,
'starts_at': get_dt_string_or_none(self.starts_at),
'finishes_at': get_dt_string_or_none(self.finishes_at),
'created_at': self.created_at.strftime(DATETIME_FORMAT) if self.created_at else None,
'approved_at': self.approved_at.strftime(DATETIME_FORMAT) if self.approved_at else None,
'cancelled_at': self.cancelled_at.strftime(DATETIME_FORMAT) if self.cancelled_at else None,
'updated_at': self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None,
'created_at': get_dt_string_or_none(self.created_at),
'approved_at': get_dt_string_or_none(self.approved_at),
'cancelled_at': get_dt_string_or_none(self.cancelled_at),
'updated_at': get_dt_string_or_none(self.updated_at),
'created_by_id': str(self.created_by_id),
'approved_by_id': str(self.approved_by_id),
'cancelled_by_id': str(self.cancelled_by_id),
}
class BroadcastEventMessageType:
ALERT = 'alert'
UPDATE = 'update'
CANCEL = 'cancel'
MESSAGE_TYPES = [ALERT, UPDATE, CANCEL]
class BroadcastEvent(db.Model):
"""
This table represents a single CAP XML blob that we sent to the mobile network providers.
We should be able to create the complete CAP message without joining from this to any other tables, eg
template, service, or broadcast_message.
The only exception to this is that we will have to join to itself to find other broadcast_events with the
same broadcast_message_id when building up the `<references>` xml field for updating/cancelling an existing message.
As such, this shouldn't have foreign keys to things that can change or be deleted.
"""
__tablename__ = 'broadcast_event'
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'))
service = db.relationship('Service')
broadcast_message_id = db.Column(UUID(as_uuid=True), db.ForeignKey('broadcast_message.id'), nullable=False)
broadcast_message = db.relationship('BroadcastMessage', backref='events')
# this is used for <sent> in the cap xml
sent_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
# msgType. alert, cancel, or update. (other options in the spec are "ack" and "error")
message_type = db.Column(db.String, nullable=False)
# this will be json containing anything that isnt hardcoded in utils/cbc proxy. for now just body but may grow to
# include, eg, title, headline, instructions.
transmitted_content = db.Column(
JSONB(none_as_null=True),
nullable=True
)
# unsubstantiated reckon: even if we're sending a cancel, we'll still need to provide areas
transmitted_areas = db.Column(JSONB(none_as_null=True), nullable=False, default=list)
transmitted_sender = db.Column(db.String(), nullable=False)
# we may only need this starts_at if this is scheduled for the future. Interested to see how this affects
# updates/cancels (ie: can you schedule an update for the future?)
transmitted_starts_at = db.Column(db.DateTime, nullable=True)
transmitted_finishes_at = db.Column(db.DateTime, nullable=True)
# @property
# def reference(self):
# # TODO: write this `from_event` function
# return BroadcastMessageTemplate.from_event(self.serialize()).reference
def serialize(self):
return {
'id': self.id,
'service_id': self.service_id,
# 'reference': self.reference,
'broadcast_message_id': self.broadcast_message_id,
'sent_at': self.sent_at,
'message_type': self.message_type,
'transmitted_content': self.transmitted_content,
'transmitted_areas': self.transmitted_areas,
'transmitted_sender': self.transmitted_sender,
'transmitted_starts_at': get_dt_string_or_none(self.transmitted_starts_at),
'transmitted_finishes_at': get_dt_string_or_none(self.transmitted_finishes_at),
}

View File

@@ -11,6 +11,7 @@ from notifications_utils.template import (
BroadcastMessageTemplate,
)
from app import DATETIME_FORMAT
local_timezone = pytz.timezone("Europe/London")
@@ -141,3 +142,7 @@ def get_notification_table_to_use(service, notification_type, process_day, has_d
def get_archived_db_column_value(column):
date = datetime.utcnow().strftime("%Y-%m-%d")
return f'_archived_{date}_{column}'
def get_dt_string_or_none(val):
return val.strftime(DATETIME_FORMAT) if val else None

View File

@@ -0,0 +1,43 @@
"""
Revision ID: 0326_broadcast_event
Revises: 0325_int_letter_rates_fix
Create Date: 2020-07-24 12:40:35.809523
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = '0326_broadcast_event'
down_revision = '0325_int_letter_rates_fix'
def upgrade():
op.create_table('broadcast_event',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('service_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('broadcast_message_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('sent_at', sa.DateTime(), nullable=False),
sa.Column('message_type', sa.String(), nullable=False),
sa.Column('transmitted_content', postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=True),
sa.Column('transmitted_areas', postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=False),
sa.Column('transmitted_sender', sa.String(), nullable=False),
sa.Column('transmitted_starts_at', sa.DateTime(), nullable=True),
sa.Column('transmitted_finishes_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['broadcast_message_id'], ['broadcast_message.id'], ),
sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
sa.PrimaryKeyConstraint('id')
)
# this shouldn't be nullable. it defaults to `[]` in python.
op.alter_column('broadcast_message', 'areas', existing_type=postgresql.JSONB(astext_type=sa.Text()), nullable=False)
# this can't be nullable. it defaults to 'draft' in python.
op.alter_column('broadcast_message', 'status', existing_type=sa.VARCHAR(), nullable=False)
op.create_foreign_key(None, 'broadcast_message', 'broadcast_status_type', ['status'], ['name'])
def downgrade():
op.drop_constraint('broadcast_message_status_fkey', 'broadcast_message', type_='foreignkey')
op.alter_column('broadcast_message', 'status', existing_type=sa.VARCHAR(), nullable=True)
op.alter_column('broadcast_message', 'areas', existing_type=postgresql.JSONB(astext_type=sa.Text()), nullable=True)
op.drop_table('broadcast_event')

View File

@@ -0,0 +1,43 @@
from datetime import datetime
from app.models import BROADCAST_TYPE
from app.models import BroadcastEventMessageType
from app.dao.broadcast_message_dao import get_earlier_events_for_broadcast_event
from tests.app.db import create_broadcast_message, create_template, create_broadcast_event
def test_get_earlier_events_for_broadcast_event(sample_service):
t = create_template(sample_service, BROADCAST_TYPE)
bm = create_broadcast_message(t)
events = [
create_broadcast_event(
bm,
sent_at=datetime(2020, 1, 1, 12, 0, 0),
message_type=BroadcastEventMessageType.ALERT,
transmitted_content={'body': 'Initial content'}
),
create_broadcast_event(
bm,
sent_at=datetime(2020, 1, 1, 13, 0, 0),
message_type=BroadcastEventMessageType.UPDATE,
transmitted_content={'body': 'Updated content'}
),
create_broadcast_event(
bm,
sent_at=datetime(2020, 1, 1, 14, 0, 0),
message_type=BroadcastEventMessageType.UPDATE,
transmitted_content={'body': 'Updated content'},
transmitted_areas=['wales']
),
create_broadcast_event(
bm,
sent_at=datetime(2020, 1, 1, 15, 0, 0),
message_type=BroadcastEventMessageType.CANCEL,
transmitted_finishes_at=datetime(2020, 1, 1, 15, 0, 0),
)
]
# only fetches earlier events, and they're in time order
earlier_events = get_earlier_events_for_broadcast_event(events[2].id)
assert earlier_events == [events[0], events[1]]

View File

@@ -62,6 +62,7 @@ from app.models import (
ServiceContactList,
BroadcastMessage,
BroadcastStatusType,
BroadcastEvent
)
@@ -1021,3 +1022,29 @@ def create_broadcast_message(
db.session.add(broadcast_message)
db.session.commit()
return broadcast_message
def create_broadcast_event(
broadcast_message,
sent_at=None,
message_type='alert',
transmitted_content=None,
transmitted_areas=None,
transmitted_sender=None,
transmitted_starts_at=None,
transmitted_finishes_at=None,
):
b_e = BroadcastEvent(
service=broadcast_message.service,
broadcast_message=broadcast_message,
sent_at=sent_at or datetime.utcnow(),
message_type=message_type,
transmitted_content=transmitted_content or {'body': 'this is an emergency broadcast message'},
transmitted_areas=transmitted_areas or ['london'],
transmitted_sender=transmitted_sender or 'www.notifications.service.gov.uk',
transmitted_starts_at=transmitted_starts_at,
transmitted_finishes_at=transmitted_finishes_at,
)
db.session.add(b_e)
db.session.commit()
return b_e

View File

@@ -117,6 +117,7 @@ def notify_db_session(notify_db, sms_providers):
"organisation_types",
"service_permission_types",
"auth_type",
"broadcast_status_type",
"invite_status_type",
"service_callback_type"]:
notify_db.engine.execute(tbl.delete())