mirror of
https://github.com/GSA/notifications-api.git
synced 2026-02-01 07:35:34 -05:00
Merge pull request #2936 from alphagov/broadcast-event-model
add broadcast_event table
This commit is contained in:
@@ -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()
|
||||
|
||||
114
app/models.py
114
app/models.py
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
43
migrations/versions/0326_broadcast_event.py
Normal file
43
migrations/versions/0326_broadcast_event.py
Normal 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')
|
||||
43
tests/app/dao/test_broadcast_message_dao.py
Normal file
43
tests/app/dao/test_broadcast_message_dao.py
Normal 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]]
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user