add broadcast message crud

new blueprint `/service/<id>/broadcast-message` with the following
endpoints:

* GET / - get all broadcast messages for a service
* GET /<id> - get a single broadcast message
* POST / - create a new broadcast message
* POST /<id> - update an existing broadcast message's data
* POST /<id>/status - move a broadcast message to a new status

I've kept the regular data update (eg personalisation, start and end
times) separate from the status update, just to keep separation of
concerns a bit more rigid, especially around who can update.

I've included schemas for the three POSTs, they're pretty
straightforward.
This commit is contained in:
Leo Hemsted
2020-07-07 11:42:38 +01:00
parent 40f747d8ef
commit 67f6dcae45
6 changed files with 243 additions and 2 deletions

View File

@@ -152,6 +152,7 @@ def register_blueprint(application):
from app.template_folder.rest import template_folder_blueprint from app.template_folder.rest import template_folder_blueprint
from app.letter_branding.letter_branding_rest import letter_branding_blueprint from app.letter_branding.letter_branding_rest import letter_branding_blueprint
from app.upload.rest import upload_blueprint from app.upload.rest import upload_blueprint
from app.broadcast_message.rest import broadcast_message_blueprint
service_blueprint.before_request(requires_admin_auth) service_blueprint.before_request(requires_admin_auth)
application.register_blueprint(service_blueprint, url_prefix='/service') application.register_blueprint(service_blueprint, url_prefix='/service')
@@ -237,6 +238,9 @@ def register_blueprint(application):
upload_blueprint.before_request(requires_admin_auth) upload_blueprint.before_request(requires_admin_auth)
application.register_blueprint(upload_blueprint) application.register_blueprint(upload_blueprint)
broadcast_message_blueprint.before_request(requires_admin_auth)
application.register_blueprint(broadcast_message_blueprint)
def register_v2_blueprints(application): def register_v2_blueprints(application):
from app.v2.inbound_sms.get_inbound_sms import v2_inbound_sms_blueprint as get_inbound_sms from app.v2.inbound_sms.get_inbound_sms import v2_inbound_sms_blueprint as get_inbound_sms

View File

View File

@@ -0,0 +1,48 @@
from app.schema_validation.definitions import uuid
from app.models import BroadcastStatusType
create_broadcast_message_schema = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'description': 'POST create broadcast_message schema',
'type': 'object',
'title': 'Create broadcast_message',
'properties': {
'template_id': uuid,
'service_id': uuid,
'created_by': uuid,
'personalisation': {'type': 'object'},
'starts_at': {'type': 'string', 'format': 'date-time'},
'finishes_at': {'type': 'string', 'format': 'date-time'},
'areas': {"type": "array", "items": {"type": "string"}},
},
'required': ['template_id', 'service_id', 'created_by'],
'additionalProperties': False
}
update_broadcast_message_schema = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'description': 'POST update broadcast_message schema',
'type': 'object',
'title': 'Update broadcast_message',
'properties': {
'personalisation': {'type': 'object'},
'starts_at': {'type': 'string', 'format': 'date-time'},
'finishes_at': {'type': 'string', 'format': 'date-time'},
'areas': {"type": "array", "items": {"type": "string"}},
},
'required': [],
'additionalProperties': False
}
update_broadcast_message_status_schema = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'description': 'POST update broadcast_message status schema',
'type': 'object',
'title': 'Update broadcast_message',
'properties': {
'status': {'type': 'string', 'enum': BroadcastStatusType.STATUSES},
'created_by': uuid,
},
'required': ['status', 'created_by'],
'additionalProperties': False
}

View File

@@ -0,0 +1,123 @@
from datetime import datetime
import iso8601
from flask import Blueprint, jsonify, request
from app.dao.templates_dao import dao_get_template_by_id_and_service_id
from app.dao.users_dao import get_user_by_id
from app.dao.broadcast_message_dao import (
dao_create_broadcast_message,
dao_get_broadcast_message_by_id_and_service_id,
dao_get_broadcast_messages_for_service,
dao_update_broadcast_message,
)
from app.dao.services_dao import dao_fetch_service_by_id
from app.errors import register_errors
from app.models import BroadcastMessage, BroadcastStatusType
from app.broadcast_message.broadcast_message_schema import (
create_broadcast_message_schema,
update_broadcast_message_schema,
update_broadcast_message_status_schema,
)
from app.schema_validation import validate
broadcast_message_blueprint = Blueprint(
'broadcast_message',
__name__,
url_prefix='/service/<uuid:service_id>/broadcast-message'
)
register_errors(broadcast_message_blueprint)
def _parse_nullable_datetime(dt):
if dt:
return iso8601.parse_date(dt).replace(tzinfo=None)
return dt
@broadcast_message_blueprint.route('', methods=['GET'])
def get_broadcast_messages_for_service(service_id):
# TODO: should this return template content/data in some way? or can we rely on them being cached admin side.
# we might need stuff like template name for showing on the dashboard.
# TODO: should this paginate or filter on dates or anything?
broadcast_messages = [o.serialize() for o in dao_get_broadcast_messages_for_service(service_id)]
return jsonify(broadcast_messages=broadcast_messages)
@broadcast_message_blueprint.route('/<uuid:broadcast_message_id>', methods=['GET'])
def get_broadcast_message(service_id, broadcast_message_id):
return jsonify(dao_get_broadcast_message_by_id_and_service_id(broadcast_message_id, service_id).serialize())
@broadcast_message_blueprint.route('', methods=['POST'])
def create_broadcast_message(service_id):
data = request.get_json()
validate(data, create_broadcast_message_schema)
service = dao_fetch_service_by_id(data['service_id'])
user = get_user_by_id(data['created_by'])
template = dao_get_template_by_id_and_service_id(data['template_id'], data['service_id'])
broadcast_message = BroadcastMessage(
service_id=service.id,
template_id=template.id,
template_version=template.version,
personalisation=data.get('personalisation', {}),
areas=data.get('areas', []),
status=BroadcastStatusType.DRAFT,
starts_at=_parse_nullable_datetime(data.get('starts_at')),
finishes_at=_parse_nullable_datetime(data.get('finishes_at')),
created_by_id=user.id,
)
dao_create_broadcast_message(broadcast_message)
return jsonify(broadcast_message.serialize()), 201
@broadcast_message_blueprint.route('/<uuid:broadcast_message_id>', methods=['POST'])
def update_broadcast_message(service_id, broadcast_message_id):
data = request.get_json()
validate(data, update_broadcast_message_schema)
broadcast_message = dao_get_broadcast_message_by_id_and_service_id(broadcast_message_id, service_id)
if 'personalisation' in data:
broadcast_message.personalisation = data['personalisation']
if 'starts_at' in data:
broadcast_message.starts_at = _parse_nullable_datetime(data['starts_at'])
if 'finishes_at' in data:
broadcast_message.starts_at = _parse_nullable_datetime(data['finishes_at'])
if 'areas' in data:
broadcast_message.areas = data['areas']
dao_update_broadcast_message(broadcast_message)
return jsonify(broadcast_message.serialize()), 200
@broadcast_message_blueprint.route('/<uuid:broadcast_message_id>/status', methods=['POST'])
def update_broadcast_message_status(service_id, broadcast_message_id):
data = request.get_json()
validate(data, update_broadcast_message_status_schema)
broadcast_message = dao_get_broadcast_message_by_id_and_service_id(broadcast_message_id, service_id)
new_status = data['status']
# TODO: Restrict status transitions
# TODO: Do we need to validate that the user belongs to the same service, isn't the creator, has permissions, etc?
# or is that admin's job
if new_status == BroadcastStatusType.BROADCASTING:
broadcast_message.approved_at = datetime.utcnow()
broadcast_message.approved_by = get_user_by_id(data['created_by'])
if new_status == BroadcastStatusType.CANCELLED:
broadcast_message.cancelled_at = datetime.utcnow()
broadcast_message.cancelled_by = get_user_by_id(data['created_by'])
broadcast_message.status = new_status
dao_update_broadcast_message(broadcast_message)
return jsonify(broadcast_message.serialize()), 200

View File

@@ -0,0 +1,26 @@
from app import db
from app.models import BroadcastMessage
from app.dao.dao_utils import transactional
@transactional
def dao_create_broadcast_message(broadcast_message):
db.session.add(broadcast_message)
@transactional
def dao_update_broadcast_message(broadcast_message):
db.session.add(broadcast_message)
def dao_get_broadcast_message_by_id_and_service_id(broadcast_message_id, service_id):
return BroadcastMessage.query.filter(
BroadcastMessage.id == broadcast_message_id,
BroadcastMessage.service_id == service_id
).one()
def dao_get_broadcast_messages_for_service(service_id):
return BroadcastMessage.query.filter(
BroadcastMessage.service_id == service_id
).order_by(BroadcastMessage.created_at)

View File

@@ -2174,7 +2174,7 @@ class BroadcastMessage(db.Model):
{} {}
) )
id = db.Column(UUID(as_uuid=True), primary_key=True) 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_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'))
service = db.relationship('Service', backref='broadcast_messages') service = db.relationship('Service', backref='broadcast_messages')
@@ -2184,6 +2184,8 @@ class BroadcastMessage(db.Model):
template = db.relationship('TemplateHistory', backref='broadcast_messages') template = db.relationship('TemplateHistory', backref='broadcast_messages')
_personalisation = db.Column(db.String, nullable=True) _personalisation = db.Column(db.String, nullable=True)
# defaults to empty list
areas = db.Column(JSONB(none_as_null=True), nullable=False, default=list)
status = db.Column( status = db.Column(
db.String, db.String,
@@ -2197,7 +2199,7 @@ class BroadcastMessage(db.Model):
finishes_at = db.Column(db.DateTime, nullable=True) # isn't updated if user cancels finishes_at = db.Column(db.DateTime, nullable=True) # isn't updated if user cancels
# these times correspond to when # these times correspond to when
created_at = db.Column(db.DateTime, nullable=False) created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
approved_at = db.Column(db.DateTime, nullable=True) approved_at = db.Column(db.DateTime, nullable=True)
cancelled_at = db.Column(db.DateTime, nullable=True) cancelled_at = db.Column(db.DateTime, nullable=True)
updated_at = db.Column(db.DateTime, nullable=True, onupdate=datetime.datetime.utcnow) updated_at = db.Column(db.DateTime, nullable=True, onupdate=datetime.datetime.utcnow)
@@ -2209,3 +2211,41 @@ class BroadcastMessage(db.Model):
created_by = db.relationship('User', foreign_keys=[created_by_id]) created_by = db.relationship('User', foreign_keys=[created_by_id])
approved_by = db.relationship('User', foreign_keys=[approved_by_id]) approved_by = db.relationship('User', foreign_keys=[approved_by_id])
cancelled_by = db.relationship('User', foreign_keys=[cancelled_by_id]) cancelled_by = db.relationship('User', foreign_keys=[cancelled_by_id])
@property
def personalisation(self):
if self._personalisation:
return encryption.decrypt(self._personalisation)
return {}
@personalisation.setter
def personalisation(self, personalisation):
self._personalisation = encryption.encrypt(personalisation or {})
def serialize(self):
return {
'id': self.id,
'service_id': self.service_id,
'template_id': self.template_id,
'template_version': self.template_version,
'template_name': self.template.name,
'personalisation': self.personalisation,
'areas': self.areas,
'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,
'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_by_id': self.created_by_id,
'approved_by_id': self.approved_by_id,
'cancelled_by_id': self.cancelled_by_id,
}