Skip to content

Commit

Permalink
Merge pull request #736 from alphagov/implement-v2-send-email
Browse files Browse the repository at this point in the history
Implemented the post email notifications endpoint for v2
  • Loading branch information
servingUpAces committed Nov 17, 2016
2 parents 34d9c44 + f5e3c6f commit 1345c94
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 44 deletions.
2 changes: 2 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ class Notification(db.Model):
onupdate=datetime.datetime.utcnow)
status = db.Column(NOTIFICATION_STATUS_TYPES_ENUM, index=True, nullable=False, default='created')
reference = db.Column(db.String, nullable=True, index=True)
client_reference = db.Column(db.String, index=True, nullable=True)
_personalisation = db.Column(db.String, nullable=True)

template_history = db.relationship('TemplateHistory', primaryjoin=and_(
Expand Down Expand Up @@ -561,6 +562,7 @@ class NotificationHistory(db.Model):
updated_at = db.Column(db.DateTime, index=False, unique=False, nullable=True)
status = db.Column(NOTIFICATION_STATUS_TYPES_ENUM, index=True, nullable=False, default='created')
reference = db.Column(db.String, nullable=True, index=True)
client_reference = db.Column(db.String, nullable=True)

@classmethod
def from_notification(cls, notification):
Expand Down
8 changes: 5 additions & 3 deletions app/notifications/process_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ def persist_notification(template_id,
key_type,
created_at=None,
job_id=None,
job_row_number=None):
job_row_number=None,
reference=None):
notification = Notification(
template_id=template_id,
template_version=template_version,
Expand All @@ -56,9 +57,10 @@ def persist_notification(template_id,
notification_type=notification_type,
api_key_id=api_key_id,
key_type=key_type,
created_at=created_at if created_at else datetime.utcnow().strftime(DATETIME_FORMAT),
created_at=created_at or datetime.utcnow().strftime(DATETIME_FORMAT),
job_id=job_id,
job_row_number=job_row_number
job_row_number=job_row_number,
client_reference=reference
)
dao_create_notification(notification)
return notification
Expand Down
36 changes: 27 additions & 9 deletions app/schema_validation/__init__.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
import json

from jsonschema import Draft4Validator, ValidationError
from jsonschema import (Draft4Validator, ValidationError, FormatChecker)
from notifications_utils.recipients import (validate_phone_number, validate_email_address, InvalidPhoneError,
InvalidEmailError)


def validate(json_to_validate, schema):
validator = Draft4Validator(schema)
format_checker = FormatChecker()

@format_checker.checks('phone_number', raises=InvalidPhoneError)
def validate_schema_phone_number(instance):
validate_phone_number(instance)
return True

@format_checker.checks('email_address', raises=InvalidEmailError)
def validate_schema_email_address(instance):
validate_email_address(instance)
return True

validator = Draft4Validator(schema, format_checker=format_checker)
errors = list(validator.iter_errors(json_to_validate))
if errors.__len__() > 0:
raise ValidationError(build_error_message(errors, schema))
raise ValidationError(build_error_message(errors))
return json_to_validate


def build_error_message(errors, schema):
def build_error_message(errors):
fields = []
for e in errors:
field = "'{}' {}".format(e.path[0], e.schema.get('validationMessage')) if e.schema.get(
'validationMessage') else e.message
s = field.split("'")
field = {"error": "ValidationError", "message": "{}{}".format(s[1], s[2])}
fields.append(field)
field = "{} {}".format(e.path[0], e.schema.get('validationMessage')) if e.schema.get(
'validationMessage') else __format_message(e)
fields.append({"error": "ValidationError", "message": field})
message = {
"status_code": 400,
"errors": fields
}

return json.dumps(message)


def __format_message(e):
s = e.message.split("'")
msg = "{}{}".format(s[1], s[2])
return msg if not e.cause else "{} {}".format(e.path[0], e.cause.message)
81 changes: 72 additions & 9 deletions app/v2/notifications/notification_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
"title": "POST v2/notifications/sms",
"properties": {
"reference": {"type": "string"},
"phone_number": {"type": "string", "format": "sms"},
"phone_number": {"type": "string", "format": "phone_number"},
"template_id": uuid,
"personalisation": personalisation
},
"required": ["phone_number", "template_id"]
}

content = {
sms_content = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "POST sms notification response schema",
"description": "content schema for SMS notification response schema",
"type": "object",
"title": "notification content",
"properties": {
Expand All @@ -29,7 +29,7 @@
# this may belong in a templates module
template = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "POST sms notification response schema",
"description": "template schema",
"type": "object",
"title": "notification content",
"properties": {
Expand All @@ -48,7 +48,50 @@
"properties": {
"id": uuid,
"reference": {"type": "string"},
"content": content,
"content": sms_content,
"uri": {"type": "string"},
"template": template
},
"required": ["id", "content", "uri", "template"]
}


post_email_request = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "POST email notification schema",
"type": "object",
"title": "POST v2/notifications/email",
"properties": {
"reference": {"type": "string"},
"email_address": {"type": "string", "format": "email_address"},
"template_id": uuid,
"personalisation": personalisation
},
"required": ["email_address", "template_id"]
}

email_content = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Email content for POST email notification",
"type": "object",
"title": "notification email content",
"properties": {
"from_email": {"type": "string", "format": "email_address"},
"body": {"type": "string"},
"subject": {"type": "string"}
},
"required": ["body", "from_email", "subject"]
}

post_email_response = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "POST sms notification response schema",
"type": "object",
"title": "response v2/notifications/email",
"properties": {
"id": uuid,
"reference": {"type": "string"},
"content": email_content,
"uri": {"type": "string"},
"template": template
},
Expand All @@ -58,11 +101,31 @@

def create_post_sms_response_from_notification(notification, body, from_number, url_root):
return {"id": notification.id,
"reference": None, # not yet implemented
"reference": notification.client_reference,
"content": {'body': body,
'from_number': from_number},
"uri": "{}/v2/notifications/{}".format(url_root, str(notification.id)),
"template": {"id": notification.template_id,
"version": notification.template_version,
"uri": "{}/v2/templates/{}".format(url_root, str(notification.template_id))}
"template": __create_template_from_notification(notification=notification, url_root=url_root)
}


def create_post_email_response_from_notification(notification, content, subject, email_from, url_root):
return {
"id": notification.id,
"reference": notification.client_reference,
"content": {
"from_email": email_from,
"body": content,
"subject": subject
},
"uri": "{}/v2/notifications/{}".format(url_root, str(notification.id)),
"template": __create_template_from_notification(notification=notification, url_root=url_root)
}


def __create_template_from_notification(notification, url_root):
return {
"id": notification.template_id,
"version": notification.template_version,
"uri": "{}/v2/templates/{}".format(url_root, str(notification.template_id))
}
56 changes: 39 additions & 17 deletions app/v2/notifications/post_notifications.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from flask import request, jsonify, current_app
from flask import request, jsonify
from sqlalchemy.orm.exc import NoResultFound

from app import api_user
from app.dao import services_dao, templates_dao
from app.models import SMS_TYPE
from app.models import SMS_TYPE, EMAIL_TYPE
from app.notifications.process_notifications import (create_content_for_notification,
persist_notification,
send_notification_to_queue)
Expand All @@ -16,7 +16,8 @@
from app.v2.errors import BadRequestError
from app.v2.notifications import notification_blueprint
from app.v2.notifications.notification_schemas import (post_sms_request,
create_post_sms_response_from_notification)
create_post_sms_response_from_notification, post_email_request,
create_post_email_response_from_notification)


@notification_blueprint.route('/sms', methods=['POST'])
Expand All @@ -27,7 +28,7 @@ def post_sms_notification():
check_service_message_limit(api_user.key_type, service)
service_can_send_to_recipient(form['phone_number'], api_user.key_type, service)

template, content = __validate_template(form, service)
template, template_with_content = __validate_template(form, service, SMS_TYPE)

notification = persist_notification(template_id=template.id,
template_version=template.version,
Expand All @@ -36,26 +37,47 @@ def post_sms_notification():
personalisation=form.get('personalisation', None),
notification_type=SMS_TYPE,
api_key_id=api_user.id,
key_type=api_user.key_type)
key_type=api_user.key_type,
reference=form['reference'])
send_notification_to_queue(notification, service.research_mode)

resp = create_post_sms_response_from_notification(notification, content, service.sms_sender, request.url_root)
resp = create_post_sms_response_from_notification(notification,
template_with_content.content,
service.sms_sender,
request.url_root)
return jsonify(resp), 201


@notification_blueprint.route('/email', methods=['POST'])
def post_email_notification():
# validate post form against post_email_request schema
# validate service
# validate template
# persist notification
# send notification to queue
# create content
# return post_email_response schema
pass
form = validate(request.get_json(), post_email_request)
service = services_dao.dao_fetch_service_by_id(api_user.service_id)

check_service_message_limit(api_user.key_type, service)
service_can_send_to_recipient(form['email_address'], api_user.key_type, service)

template, template_with_content = __validate_template(form, service, EMAIL_TYPE)
notification = persist_notification(template_id=template.id,
template_version=template.version,
recipient=form['email_address'],
service_id=service.id,
personalisation=form.get('personalisation', None),
notification_type=EMAIL_TYPE,
api_key_id=api_user.id,
key_type=api_user.key_type,
reference=form['reference'])

send_notification_to_queue(notification, service.research_mode)

resp = create_post_email_response_from_notification(notification=notification,
content=template_with_content.content,
subject=template_with_content.subject,
email_from=service.email_from,
url_root=request.url_root)
return jsonify(resp), 201


def __validate_template(form, service):
def __validate_template(form, service, notification_type):
try:
template = templates_dao.dao_get_template_by_id_and_service_id(template_id=form['template_id'],
service_id=service.id)
Expand All @@ -64,8 +86,8 @@ def __validate_template(form, service):
raise BadRequestError(message=message,
fields=[{'template': message}])

check_template_is_for_notification_type(SMS_TYPE, template.template_type)
check_template_is_for_notification_type(notification_type, template.template_type)
check_template_is_active(template)
template_with_content = create_content_for_notification(template, form.get('personalisation', {}))
check_sms_content_char_count(template_with_content.replaced_content_count)
return template, template_with_content.content
return template, template_with_content
24 changes: 24 additions & 0 deletions migrations/versions/0061_add_client_reference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""empty message
Revision ID: 0061_add_client_reference
Revises: 0060_add_letter_template_type
Create Date: 2016-11-17 13:19:25.820617
"""

# revision identifiers, used by Alembic.
revision = '0061_add_client_reference'
down_revision = '0060_add_letter_template_type'

from alembic import op
import sqlalchemy as sa


def upgrade():
op.add_column('notifications', sa.Column('client_reference', sa.String(), index=True, nullable=True))
op.add_column('notification_history', sa.Column('client_reference', sa.String(), nullable=True))


def downgrade():
op.drop_column('notifications', 'client_reference')
op.drop_column('notification_history', 'client_reference')
7 changes: 5 additions & 2 deletions tests/app/notifications/test_process_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def test_persist_notification_throws_exception_when_missing_template(sample_api_
assert NotificationHistory.query.count() == 0


def test_persist_notification_with_job_and_created(sample_job, sample_api_key):
def test_persist_notification_with_optionals(sample_job, sample_api_key):
assert Notification.query.count() == 0
assert NotificationHistory.query.count() == 0
created_at = datetime.datetime(2016, 11, 11, 16, 8, 18)
Expand All @@ -77,13 +77,16 @@ def test_persist_notification_with_job_and_created(sample_job, sample_api_key):
key_type=sample_api_key.key_type,
created_at=created_at,
job_id=sample_job.id,
job_row_number=10)
job_row_number=10,
reference="ref from client")
assert Notification.query.count() == 1
assert NotificationHistory.query.count() == 1
persisted_notification = Notification.query.all()[0]
assert persisted_notification.job_id == sample_job.id
assert persisted_notification.job_row_number == 10
assert persisted_notification.created_at == created_at
assert persisted_notification.client_reference == "ref from client"
assert persisted_notification.reference is None


@pytest.mark.parametrize('research_mode, queue, notification_type, key_type',
Expand Down

0 comments on commit 1345c94

Please sign in to comment.