Merge remote-tracking branch 'origin/master' into remove_alpha_client_from_api

This commit is contained in:
Nicholas Staples
2016-02-09 15:54:21 +00:00
10 changed files with 419 additions and 5 deletions

View File

@@ -0,0 +1,22 @@
from app import db
from app.models import Notification
def save_notification(notification, update_dict={}):
if update_dict:
update_dict.pop('id', None)
update_dict.pop('job', None)
update_dict.pop('service', None)
update_dict.pop('template', None)
Notification.query.filter_by(id=notification.id).update(update_dict)
else:
db.session.add(notification)
db.session.commit()
def get_notification(service_id, job_id, notification_id):
return Notification.query.filter_by(service_id=service_id, job_id=job_id, id=notification_id).one()
def get_notifications(service_id, job_id):
return Notification.query.filter_by(service_id=service_id, job_id=job_id).all()

View File

@@ -8,7 +8,6 @@ from flask import (
current_app current_app
) )
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.exc import DataError from sqlalchemy.exc import DataError
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
@@ -18,10 +17,19 @@ from app.dao.jobs_dao import (
get_jobs_by_service get_jobs_by_service
) )
from app.dao.notifications_dao import (
save_notification,
get_notification,
get_notifications
)
from app.schemas import ( from app.schemas import (
job_schema, job_schema,
jobs_schema, jobs_schema,
job_schema_load_json job_schema_load_json,
notification_status_schema,
notifications_status_schema,
notification_status_schema_load_json
) )
job = Blueprint('job', __name__, url_prefix='/service/<service_id>/job') job = Blueprint('job', __name__, url_prefix='/service/<service_id>/job')
@@ -72,6 +80,55 @@ def update_job(service_id, job_id):
return jsonify(data=job_schema.dump(job).data), 200 return jsonify(data=job_schema.dump(job).data), 200
@job.route('/<job_id>/notification', methods=['POST'])
def create_notification_for_job(service_id, job_id):
# TODO assert service_id == payload service id
# and same for job id
notification, errors = notification_status_schema.load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
try:
save_notification(notification)
except Exception as e:
return jsonify(result="error", message=str(e)), 500
return jsonify(data=notification_status_schema.dump(notification).data), 201
@job.route('/<job_id>/notification', methods=['GET'])
@job.route('/<job_id>/notification/<notification_id>')
def get_notification_for_job(service_id, job_id, notification_id=None):
if notification_id:
try:
notification = get_notification(service_id, job_id, notification_id)
data, errors = notification_status_schema.dump(notification)
return jsonify(data=data)
except DataError:
return jsonify(result="error", message="Invalid notification id"), 400
except NoResultFound:
return jsonify(result="error", message="Notification not found"), 404
else:
notifications = get_notifications(service_id, job_id)
data, errors = notifications_status_schema.dump(notifications)
return jsonify(data=data)
@job.route('/<job_id>/notification/<notification_id>', methods=['PUT'])
def update_notification_for_job(service_id, job_id, notification_id):
notification = get_notification(service_id, job_id, notification_id)
update_dict, errors = notification_status_schema_load_json.load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
try:
save_notification(notification, update_dict=update_dict)
except Exception as e:
return jsonify(result="error", message=str(e)), 400
return jsonify(data=job_schema.dump(notification).data), 200
def _enqueue_job(job): def _enqueue_job(job):
aws_region = current_app.config['AWS_REGION'] aws_region = current_app.config['AWS_REGION']
queue_name = current_app.config['NOTIFY_JOB_QUEUE'] queue_name = current_app.config['NOTIFY_JOB_QUEUE']

View File

@@ -189,3 +189,34 @@ class VerifyCode(db.Model):
def check_code(self, cde): def check_code(self, cde):
return check_hash(cde, self._code) return check_hash(cde, self._code)
NOTIFICATION_STATUS_TYPES = ['sent', 'failed']
class Notification(db.Model):
__tablename__ = 'notifications'
id = db.Column(UUID(as_uuid=True), primary_key=True)
to = db.Column(db.String, nullable=False)
job_id = db.Column(UUID(as_uuid=True), db.ForeignKey('jobs.id'), index=True, unique=False, nullable=False)
job = db.relationship('Job', backref=db.backref('notifications', lazy='dynamic'))
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, unique=False)
service = db.relationship('Service')
template_id = db.Column(db.BigInteger, db.ForeignKey('templates.id'), index=True, unique=False)
template = db.relationship('Template')
created_at = db.Column(
db.DateTime,
index=False,
unique=False,
nullable=False,
default=datetime.datetime.now)
updated_at = db.Column(
db.DateTime,
index=False,
unique=False,
nullable=True,
onupdate=datetime.datetime.now)
status = db.Column(
db.Enum(*NOTIFICATION_STATUS_TYPES, name='notification_status_types'), nullable=False, default='sent')

View File

@@ -129,6 +129,12 @@ class EmailNotificationSchema(NotificationSchema):
body = fields.Str(load_from="message", dump_to='message', required=True) body = fields.Str(load_from="message", dump_to='message', required=True)
class NotificationStatusSchema(BaseSchema):
class Meta:
model = models.Notification
user_schema = UserSchema() user_schema = UserSchema()
user_schema_load_json = UserSchema(load_json=True) user_schema_load_json = UserSchema(load_json=True)
users_schema = UserSchema(many=True) users_schema = UserSchema(many=True)
@@ -148,3 +154,6 @@ request_verify_code_schema = RequestVerifyCodeSchema()
sms_admin_notification_schema = SmsAdminNotificationSchema() sms_admin_notification_schema = SmsAdminNotificationSchema()
sms_template_notification_schema = SmsTemplateNotificationSchema() sms_template_notification_schema = SmsTemplateNotificationSchema()
email_notification_schema = EmailNotificationSchema() email_notification_schema = EmailNotificationSchema()
notification_status_schema = NotificationStatusSchema()
notifications_status_schema = NotificationStatusSchema(many=True)
notification_status_schema_load_json = NotificationStatusSchema(load_json=True)

View File

@@ -30,18 +30,30 @@ class Development(Config):
ADMIN_CLIENT_SECRET = 'dev-notify-secret-key' ADMIN_CLIENT_SECRET = 'dev-notify-secret-key'
DELIVERY_CLIENT_USER_NAME = 'dev-notify-delivery' DELIVERY_CLIENT_USER_NAME = 'dev-notify-delivery'
DELIVERY_CLIENT_SECRET = 'dev-notify-secret-key' DELIVERY_CLIENT_SECRET = 'dev-notify-secret-key'
NOTIFICATION_QUEUE_PREFIX = 'notification_development'
class Test(Development): class Test(Development):
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/test_notification_api' SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/test_notification_api'
NOTIFICATION_QUEUE_PREFIX = 'notification_test'
class Preview(Config):
NOTIFICATION_QUEUE_PREFIX = 'notification_preview'
class Staging(Config):
NOTIFICATION_QUEUE_PREFIX = 'notification_staging'
class Live(Config): class Live(Config):
pass NOTIFICATION_QUEUE_PREFIX = 'notification_live'
configs = { configs = {
'development': Development, 'development': Development,
'preview': Preview,
'staging': Staging,
'test': Test, 'test': Test,
'live': Live, 'live': Live,
} }

View File

@@ -0,0 +1,47 @@
"""empty message
Revision ID: 0013_add_notifications
Revises: 0012_add_status_to_job
Create Date: 2016-02-09 11:14:46.708551
"""
# revision identifiers, used by Alembic.
revision = '0013_add_notifications'
down_revision = '0012_add_status_to_job'
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('notifications',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('to', sa.String(), nullable=False),
sa.Column('job_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('service_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('template_id', sa.BigInteger(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('status', sa.Enum('sent', 'failed', name='notification_status_types'), nullable=False),
sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], ),
sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
sa.ForeignKeyConstraint(['template_id'], ['templates.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_notifications_job_id'), 'notifications', ['job_id'], unique=False)
op.create_index(op.f('ix_notifications_service_id'), 'notifications', ['service_id'], unique=False)
op.create_index(op.f('ix_notifications_template_id'), 'notifications', ['template_id'], unique=False)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_notifications_template_id'), table_name='notifications')
op.drop_index(op.f('ix_notifications_service_id'), table_name='notifications')
op.drop_index(op.f('ix_notifications_job_id'), table_name='notifications')
op.drop_table('notifications')
op.get_bind()
op.execute("drop type notification_status_types")
### end Alembic commands ###

View File

@@ -1,12 +1,13 @@
import pytest import pytest
from flask import jsonify from flask import jsonify
from app.models import (User, Service, Template, ApiKey, Job, VerifyCode) from app.models import (User, Service, Template, ApiKey, Job, VerifyCode, Notification)
from app.dao.users_dao import (save_model_user, create_user_code, create_secret_code) from app.dao.users_dao import (save_model_user, create_user_code, create_secret_code)
from app.dao.services_dao import save_model_service from app.dao.services_dao import save_model_service
from app.dao.templates_dao import save_model_template from app.dao.templates_dao import save_model_template
from app.dao.api_key_dao import save_model_api_key from app.dao.api_key_dao import save_model_api_key
from app.dao.jobs_dao import save_job from app.dao.jobs_dao import save_job
from app.dao.notifications_dao import save_notification
import uuid import uuid
@@ -162,3 +163,31 @@ def mock_secret_code(mocker):
mock_class = mocker.patch('app.dao.users_dao.create_secret_code', side_effect=_create) mock_class = mocker.patch('app.dao.users_dao.create_secret_code', side_effect=_create)
return mock_class return mock_class
@pytest.fixture(scope='function')
def sample_notification(notify_db,
notify_db_session,
service=None,
template=None,
job=None):
if service is None:
service = sample_service(notify_db, notify_db_session)
if template is None:
template = sample_template(notify_db, notify_db_session, service=service)
if job is None:
job = sample_job(notify_db, notify_db_session, service=service, template=template)
notificaton_id = uuid.uuid4()
to = '+44709123456'
data = {
'id': notificaton_id,
'to': to,
'job': job,
'service': service,
'template': template
}
notification = Notification(**data)
save_notification(notification)
return notification

View File

@@ -0,0 +1,75 @@
import uuid
from app.models import Notification
from app.dao.notifications_dao import (
save_notification,
get_notification,
get_notifications
)
def test_save_notification(notify_db, notify_db_session, sample_template, sample_job):
assert Notification.query.count() == 0
notification_id = uuid.uuid4()
to = '+44709123456'
data = {
'id': notification_id,
'to': to,
'job': sample_job,
'service': sample_template.service,
'template': sample_template
}
notification = Notification(**data)
save_notification(notification)
assert Notification.query.count() == 1
notification_from_db = Notification.query.get(notification_id)
assert data['id'] == notification_from_db.id
assert data['to'] == notification_from_db.to
assert data['job'] == notification_from_db.job
assert data['service'] == notification_from_db.service
assert data['template'] == notification_from_db.template
assert 'sent' == notification_from_db.status
def test_get_notification_for_job(notify_db, notify_db_session, sample_notification):
notifcation_from_db = get_notification(sample_notification.service.id,
sample_notification.job_id,
sample_notification.id)
assert sample_notification == notifcation_from_db
def test_get_all_notifications_for_job(notify_db, notify_db_session, sample_job):
from tests.app.conftest import sample_notification
for i in range(0, 5):
sample_notification(notify_db,
notify_db_session,
service=sample_job.service,
template=sample_job.template,
job=sample_job)
notifcations_from_db = get_notifications(sample_job.service.id, sample_job.id)
assert len(notifcations_from_db) == 5
def test_update_notification(notify_db, notify_db_session, sample_notification):
assert sample_notification.status == 'sent'
update_dict = {
'id': str(sample_notification.id),
'service': str(sample_notification.service.id),
'template': sample_notification.template.id,
'job': str(sample_notification.job.id),
'status': 'failed'
}
save_notification(sample_notification, update_dict=update_dict)
notification_from_db = Notification.query.get(sample_notification.id)
assert notification_from_db.status == 'failed'

View File

@@ -163,6 +163,130 @@ def test_get_update_job_status(notify_api,
assert resp_json['data']['status'] == 'in progress' assert resp_json['data']['status'] == 'in progress'
def test_get_notification(notify_api, notify_db, notify_db_session, sample_notification):
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = url_for('job.get_notification_for_job',
service_id=sample_notification.service.id,
job_id=sample_notification.job.id,
notification_id=sample_notification.id)
auth_header = create_authorization_header(service_id=sample_notification.service.id,
path=path,
method='GET')
headers = [('Content-Type', 'application/json'), auth_header]
response = client.get(path, headers=headers)
resp_json = json.loads(response.get_data(as_text=True))
assert str(sample_notification.id) == resp_json['data']['id']
assert str(sample_notification.service.id) == resp_json['data']['service']
assert sample_notification.template.id == resp_json['data']['template']
assert str(sample_notification.job.id) == resp_json['data']['job']
assert sample_notification.status == resp_json['data']['status']
def test_get_notifications(notify_api, notify_db, notify_db_session, sample_job):
from tests.app.conftest import sample_notification
for i in range(0, 5):
sample_notification(notify_db,
notify_db_session,
service=sample_job.service,
template=sample_job.template,
job=sample_job)
service_id = str(sample_job.service.id)
job_id = str(sample_job.id)
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = url_for('job.get_notification_for_job',
service_id=service_id,
job_id=job_id)
auth_header = create_authorization_header(service_id=service_id,
path=path,
method='GET')
headers = [('Content-Type', 'application/json'), auth_header]
response = client.get(path, headers=headers)
resp_json = json.loads(response.get_data(as_text=True))
assert len(resp_json['data']) == 5
def test_add_notification(notify_api, notify_db, notify_db_session, sample_job):
notificaton_id = uuid.uuid4()
to = '+44709123456'
data = {
'id': str(notificaton_id),
'to': to,
'job': str(sample_job.id),
'service': str(sample_job.service.id),
'template': sample_job.template.id
}
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = url_for('job.create_notification_for_job',
service_id=sample_job.service.id,
job_id=sample_job.id)
auth_header = create_authorization_header(service_id=sample_job.service.id,
path=path,
method='POST',
request_body=json.dumps(data))
headers = [('Content-Type', 'application/json'), auth_header]
response = client.post(path, headers=headers, data=json.dumps(data))
resp_json = json.loads(response.get_data(as_text=True))
assert data['id'] == resp_json['data']['id']
assert data['to'] == resp_json['data']['to']
assert data['service'] == resp_json['data']['service']
assert data['template'] == resp_json['data']['template']
assert data['job'] == resp_json['data']['job']
assert 'sent' == resp_json['data']['status']
def test_update_notification(notify_api, notify_db, notify_db_session, sample_notification):
assert sample_notification.status == 'sent'
update_data = {
'id': str(sample_notification.id),
'to': sample_notification.to,
'job': str(sample_notification.job.id),
'service': str(sample_notification.service.id),
'template': sample_notification.template.id,
'status': 'failed'
}
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = url_for('job.update_notification_for_job',
service_id=sample_notification.service.id,
job_id=sample_notification.job.id,
notification_id=sample_notification.id)
auth_header = create_authorization_header(service_id=sample_notification.service.id,
path=path,
method='PUT',
request_body=json.dumps(update_data))
headers = [('Content-Type', 'application/json'), auth_header]
response = client.put(path, headers=headers, data=json.dumps(update_data))
resp_json = json.loads(response.get_data(as_text=True))
assert update_data['id'] == resp_json['data']['id']
assert 'failed' == resp_json['data']['status']
def _setup_jobs(notify_db, notify_db_session, template, number_of_jobs=5): def _setup_jobs(notify_db, notify_db_session, template, number_of_jobs=5):
for i in range(number_of_jobs): for i in range(number_of_jobs):
create_job(notify_db, notify_db_session, service=template.service, create_job(notify_db, notify_db_session, service=template.service,

10
wsgi.py
View File

@@ -1,9 +1,17 @@
from app import create_app from app import create_app
from credstash import getAllSecrets from credstash import getAllSecrets
import os
config = 'live'
default_env_file = '/home/ubuntu/environment'
if os.path.isfile(default_env_file):
environment = open(default_env_file, 'r')
config = environment.readline().strip()
secrets = getAllSecrets(region="eu-west-1") secrets = getAllSecrets(region="eu-west-1")
application = create_app('live', secrets) application = create_app(config, secrets)
if __name__ == "__main__": if __name__ == "__main__":
application.run() application.run()