Skip to content

Commit

Permalink
Merge pull request #740 from alphagov/pc-newly-nab-notifications
Browse files Browse the repository at this point in the history
[#131977833] Create GET notification by ID endpoint for V2 API
  • Loading branch information
pcraig3 committed Nov 22, 2016
2 parents 1345c94 + ebfac18 commit 0e0dbe6
Show file tree
Hide file tree
Showing 24 changed files with 341 additions and 111 deletions.
9 changes: 6 additions & 3 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from app.encryption import Encryption


DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
DATE_FORMAT = "%Y-%m-%d"

db = SQLAlchemy()
Expand Down Expand Up @@ -96,8 +96,11 @@ def register_blueprint(application):


def register_v2_blueprints(application):
from app.v2.notifications.post_notifications import notification_blueprint
application.register_blueprint(notification_blueprint)
from app.v2.notifications.post_notifications import notification_blueprint as post_notifications
from app.v2.notifications.get_notifications import notification_blueprint as get_notifications

application.register_blueprint(post_notifications)
application.register_blueprint(get_notifications)


def init_app(app):
Expand Down
68 changes: 66 additions & 2 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import uuid
import datetime
from flask import url_for

from sqlalchemy.dialects.postgresql import (
UUID,
JSON
)
from sqlalchemy import UniqueConstraint, and_
from sqlalchemy import UniqueConstraint, and_, desc
from sqlalchemy.orm import foreign, remote
from notifications_utils.recipients import (
validate_email_address,
Expand All @@ -22,7 +23,8 @@
from app import (
db,
encryption,
DATETIME_FORMAT)
DATETIME_FORMAT
)

from app.history_meta import Versioned

Expand Down Expand Up @@ -281,6 +283,10 @@ class Template(db.Model):
created_by = db.relationship('User')
version = db.Column(db.Integer, default=1, nullable=False)

def get_link(self):
# TODO: use "/v2/" route once available
return url_for("template.get_template_by_id_and_service_id", service_id=self.service_id, template_id=self.id)


class TemplateHistory(db.Model):
__tablename__ = 'templates_history'
Expand Down Expand Up @@ -538,6 +544,64 @@ def personalisation(self, personalisation):
if personalisation:
self._personalisation = encryption.encrypt(personalisation)

def cost(self):
if not self.sent_by or self.billable_units == 0:
return 0

provider_rate = db.session.query(
ProviderRates
).join(ProviderDetails).filter(
ProviderDetails.identifier == self.sent_by,
ProviderRates.provider_id == ProviderDetails.id
).order_by(
desc(ProviderRates.valid_from)
).limit(1).one()

return provider_rate.rate * self.billable_units

def completed_at(self):
if self.status in [
NOTIFICATION_DELIVERED,
NOTIFICATION_FAILED,
NOTIFICATION_TECHNICAL_FAILURE,
NOTIFICATION_TEMPORARY_FAILURE,
NOTIFICATION_PERMANENT_FAILURE
]:
return self.updated_at.strftime(DATETIME_FORMAT)

return None

def serialize(self):

template_dict = {
'version': self.template.version,
'id': self.template.id,
'uri': self.template.get_link()
}

serialized = {
"id": self.id,
"reference": self.client_reference,
"email_address": self.to if self.notification_type == EMAIL_TYPE else None,
"phone_number": self.to if self.notification_type == SMS_TYPE else None,
"line_1": None,
"line_2": None,
"line_3": None,
"line_4": None,
"line_5": None,
"line_6": None,
"postcode": None,
"cost": self.cost(),
"type": self.notification_type,
"status": self.status,
"template": template_dict,
"created_at": self.created_at.strftime(DATETIME_FORMAT),
"sent_at": self.sent_at.strftime(DATETIME_FORMAT) if self.sent_at else None,
"completed_at": self.completed_at()
}

return serialized


class NotificationHistory(db.Model):
__tablename__ = 'notification_history'
Expand Down
6 changes: 4 additions & 2 deletions app/schema_validation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ def validate(json_to_validate, schema):

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

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

validator = Draft4Validator(schema, format_checker=format_checker)
Expand Down
10 changes: 9 additions & 1 deletion app/v2/notifications/get_notifications.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
from flask import jsonify

from app import api_user
from app.dao import notifications_dao
from app.v2.notifications import notification_blueprint


@notification_blueprint.route("/<uuid:id>", methods=['GET'])
def get_notification_by_id(id):
pass
notification = notifications_dao.get_notification_with_personalisation(
str(api_user.service_id), id, key_type=None
)

return jsonify(notification.serialize()), 200


@notification_blueprint.route("/", methods=['GET'])
Expand Down
88 changes: 74 additions & 14 deletions app/v2/notifications/notification_schemas.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,79 @@
from app.schema_validation.definitions import (uuid, personalisation)

# this may belong in a templates module
template = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "template schema",
"type": "object",
"title": "notification content",
"properties": {
"id": uuid,
"version": {"type": "integer"},
"uri": {"type": "string"}
},
"required": ["id", "version", "uri"]
}

get_notification_response = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "GET notification response schema",
"type": "object",
"title": "response v2/notification",
"oneOf": [
{"properties": {
"email_address": {"type": "string", "format": "email_address"},
"type": {"enum": ["email"]},

"phone_number": {"type": "null"},
"line_1": {"type": "null"},
"postcode": {"type": "null"}
}},
{"properties": {
"phone_number": {"type": "string", "format": "phone_number"},
"type": {"enum": ["sms"]},

"email_address": {"type": "null"},
"line_1": {"type": "null"},
"postcode": {"type": "null"}
}},
{"properties": {
"line_1": {"type": "string", "minLength": 1},
"postcode": {"type": "string", "minLength": 1},
"type": {"enum": ["letter"]},

"email_address": {"type": "null"},
"phone_number": {"type": "null"}
}}
],
"properties": {
"id": uuid,
"reference": {"type": ["string", "null"]},
"email_address": {"type": ["string", "null"]},
"phone_number": {"type": ["string", "null"]},
"line_1": {"type": ["string", "null"]},
"line_2": {"type": ["string", "null"]},
"line_3": {"type": ["string", "null"]},
"line_4": {"type": ["string", "null"]},
"line_5": {"type": ["string", "null"]},
"line_6": {"type": ["string", "null"]},
"postcode": {"type": ["string", "null"]},
"cost": {"type": "number"},
"type": {"enum": ["sms", "letter", "email"]},
"status": {"type": "string"},
"template": template,
"created_at": {"type": "string"},
"sent_at": {"type": ["string", "null"]},
"completed_at": {"type": ["string", "null"]}
},
"required": [
# technically, all keys are required since we always have all of them
"id", "reference", "email_address", "phone_number",
"line_1", "line_2", "line_3", "line_4", "line_5", "line_6", "postcode",
"cost", "type", "status", "template",
"created_at", "sent_at", "completed_at"
]
}

post_sms_request = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "POST sms notification schema",
Expand All @@ -26,20 +100,6 @@
"required": ["body"]
}

# this may belong in a templates module
template = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "template schema",
"type": "object",
"title": "notification content",
"properties": {
"id": uuid,
"version": {"type": "integer"},
"uri": {"type": "string"}
},
"required": ["id", "version", "uri"]
}

post_sms_response = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "POST sms notification response schema",
Expand Down
10 changes: 5 additions & 5 deletions tests/app/celery/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def test_should_process_sms_job(sample_job, mocker):
(str(sample_job.service_id),
"uuid",
"something_encrypted",
"2016-01-01T11:09:00.061258"),
"2016-01-01T11:09:00.061258Z"),
queue="db-sms"
)
job = jobs_dao.dao_get_job_by_id(sample_job.id)
Expand Down Expand Up @@ -104,7 +104,7 @@ def test_should_process_sms_job_into_research_mode_queue_if_research_mode_servic
(str(job.service_id),
"uuid",
"something_encrypted",
"2016-01-01T11:09:00.061258"),
"2016-01-01T11:09:00.061258Z"),
queue="research-mode"
)

Expand Down Expand Up @@ -133,7 +133,7 @@ def test_should_process_email_job_into_research_mode_queue_if_research_mode_serv
(str(job.service_id),
"uuid",
"something_encrypted",
"2016-01-01T11:09:00.061258"),
"2016-01-01T11:09:00.061258Z"),
queue="research-mode"
)

Expand Down Expand Up @@ -252,7 +252,7 @@ def test_should_process_email_job_if_exactly_on_send_limits(notify_db,
str(job.service_id),
"uuid",
"something_encrypted",
"2016-01-01T11:09:00.061258"
"2016-01-01T11:09:00.061258Z"
),
queue="db-email"
)
Expand Down Expand Up @@ -294,7 +294,7 @@ def test_should_process_email_job(sample_email_job, mocker):
str(sample_email_job.service_id),
"uuid",
"something_encrypted",
"2016-01-01T11:09:00.061258"
"2016-01-01T11:09:00.061258Z"
),
queue="db-email"
)
Expand Down
16 changes: 14 additions & 2 deletions tests/app/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from app.dao.jobs_dao import dao_create_job
from app.dao.notifications_dao import dao_create_notification
from app.dao.invited_user_dao import save_invited_user
from app.dao.provider_rates_dao import create_provider_rates
from app.clients.sms.firetext import FiretextClient


Expand Down Expand Up @@ -409,7 +410,8 @@ def sample_notification(notify_db,
create=True,
personalisation=None,
api_key_id=None,
key_type=KEY_TYPE_NORMAL):
key_type=KEY_TYPE_NORMAL,
sent_by=None):
if created_at is None:
created_at = datetime.utcnow()
if service is None:
Expand Down Expand Up @@ -441,7 +443,8 @@ def sample_notification(notify_db,
'personalisation': personalisation,
'notification_type': template.template_type,
'api_key_id': api_key_id,
'key_type': key_type
'key_type': key_type,
'sent_by': sent_by
}
if job_row_number:
data['job_row_number'] = job_row_number
Expand Down Expand Up @@ -841,3 +844,12 @@ def sample_service_whitelist(notify_db, notify_db_session, service=None, email_a
notify_db.session.add(whitelisted_user)
notify_db.session.commit()
return whitelisted_user


@pytest.fixture(scope='function')
def sample_provider_rate(notify_db, notify_db_session, valid_from=None, rate=None, provider_identifier=None):
create_provider_rates(
provider_identifier=provider_identifier if provider_identifier is not None else 'mmg',
valid_from=valid_from if valid_from is not None else datetime.utcnow(),
rate=rate if rate is not None else 1,
)
2 changes: 1 addition & 1 deletion tests/app/invite/test_invite_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_create_invited_user(notify_api, sample_service, mocker, invitation_emai
(str(current_app.config['NOTIFY_SERVICE_ID']),
'some_uuid',
encryption.encrypt(message),
"2016-01-01T11:09:00.061258"),
"2016-01-01T11:09:00.061258Z"),
queue="notify")


Expand Down
16 changes: 13 additions & 3 deletions tests/app/public_contracts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@

from flask import json
import jsonschema
from jsonschema import Draft4Validator


def validate(json_string, schema_filename):
schema_dir = os.path.join(os.path.dirname(__file__), 'schemas')
def return_json_from_response(response):
return json.loads(response.get_data(as_text=True))


def validate_v0(json_to_validate, schema_filename):
schema_dir = os.path.join(os.path.dirname(__file__), 'schemas/v0')
resolver = jsonschema.RefResolver('file://' + schema_dir + '/', None)
with open(os.path.join(schema_dir, schema_filename)) as schema:
jsonschema.validate(
json.loads(json_string),
json_to_validate,
json.load(schema),
format_checker=jsonschema.FormatChecker(),
resolver=resolver
)


def validate(json_to_validate, schema):
validator = Draft4Validator(schema)
validator.validate(json_to_validate, schema)

0 comments on commit 0e0dbe6

Please sign in to comment.