mirror of
https://github.com/GSA/notifications-api.git
synced 2026-08-24 16:23:44 -04:00
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:
@@ -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
|
||||||
|
|||||||
0
app/broadcast_message/__init__.py
Normal file
0
app/broadcast_message/__init__.py
Normal file
48
app/broadcast_message/broadcast_message_schema.py
Normal file
48
app/broadcast_message/broadcast_message_schema.py
Normal 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
|
||||||
|
}
|
||||||
123
app/broadcast_message/rest.py
Normal file
123
app/broadcast_message/rest.py
Normal 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
|
||||||
26
app/dao/broadcast_message_dao.py
Normal file
26
app/dao/broadcast_message_dao.py
Normal 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)
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user