Merge branch 'master' into integrate_MMG

Conflicts:
	app/notifications/rest.py
This commit is contained in:
Rebecca Law
2016-04-06 14:58:13 +01:00
21 changed files with 770 additions and 267 deletions
+2
View File
@@ -59,6 +59,7 @@ def create_app(app_name=None):
from app.permission.rest import permission as permission_blueprint from app.permission.rest import permission as permission_blueprint
from app.accept_invite.rest import accept_invite from app.accept_invite.rest import accept_invite
from app.notifications_statistics.rest import notifications_statistics as notifications_statistics_blueprint from app.notifications_statistics.rest import notifications_statistics as notifications_statistics_blueprint
from app.template_statistics.rest import template_statistics as template_statistics_blueprint
application.register_blueprint(service_blueprint, url_prefix='/service') application.register_blueprint(service_blueprint, url_prefix='/service')
application.register_blueprint(user_blueprint, url_prefix='/user') application.register_blueprint(user_blueprint, url_prefix='/user')
@@ -70,6 +71,7 @@ def create_app(app_name=None):
application.register_blueprint(permission_blueprint, url_prefix='/permission') application.register_blueprint(permission_blueprint, url_prefix='/permission')
application.register_blueprint(accept_invite, url_prefix='/invite') application.register_blueprint(accept_invite, url_prefix='/invite')
application.register_blueprint(notifications_statistics_blueprint) application.register_blueprint(notifications_statistics_blueprint)
application.register_blueprint(template_statistics_blueprint)
return application return application
+12 -3
View File
@@ -1,7 +1,16 @@
from boto3 import resource from boto3 import resource
def get_job_from_s3(bucket_name, job_id): def get_s3_job_object(bucket_name, job_id):
s3 = resource('s3') s3 = resource('s3')
key = s3.Object(bucket_name, '{}.csv'.format(job_id)) return s3.Object(bucket_name, '{}.csv'.format(job_id))
return key.get()['Body'].read().decode('utf-8')
def get_job_from_s3(bucket_name, job_id):
obj = get_s3_job_object(bucket_name, job_id)
return obj.get()['Body'].read().decode('utf-8')
def remove_job_from_s3(bucket_name, job_id):
obj = get_s3_job_object(bucket_name, job_id)
return obj.delete()
+92 -77
View File
@@ -1,3 +1,4 @@
import itertools
from datetime import datetime from datetime import datetime
from flask import current_app from flask import current_app
@@ -12,7 +13,8 @@ from utils.template import Template
from utils.recipients import ( from utils.recipients import (
RecipientCSV, RecipientCSV,
validate_and_format_phone_number validate_and_format_phone_number,
allowed_to_send_to
) )
from app import ( from app import (
@@ -33,8 +35,7 @@ from app.dao.invited_user_dao import delete_invitations_created_more_than_two_da
from app.dao.notifications_dao import ( from app.dao.notifications_dao import (
dao_create_notification, dao_create_notification,
dao_update_notification, dao_update_notification,
delete_failed_notifications_created_more_than_a_week_ago, delete_notifications_created_more_than_a_week_ago,
delete_successful_notifications_created_more_than_a_day_ago,
dao_get_notification_statistics_for_service_and_day, dao_get_notification_statistics_for_service_and_day,
update_notification_reference_by_id update_notification_reference_by_id
) )
@@ -50,11 +51,6 @@ from app.models import (
TEMPLATE_TYPE_SMS TEMPLATE_TYPE_SMS
) )
from app.validation import (
allowed_send_to_email,
allowed_send_to_number
)
@notify_celery.task(name="delete-verify-codes") @notify_celery.task(name="delete-verify-codes")
def delete_verify_codes(): def delete_verify_codes():
@@ -73,7 +69,7 @@ def delete_verify_codes():
def delete_successful_notifications(): def delete_successful_notifications():
try: try:
start = datetime.utcnow() start = datetime.utcnow()
deleted = delete_successful_notifications_created_more_than_a_day_ago() deleted = delete_notifications_created_more_than_a_week_ago('sent')
current_app.logger.info( current_app.logger.info(
"Delete job started {} finished {} deleted {} successful notifications".format( "Delete job started {} finished {} deleted {} successful notifications".format(
start, start,
@@ -90,7 +86,7 @@ def delete_successful_notifications():
def delete_failed_notifications(): def delete_failed_notifications():
try: try:
start = datetime.utcnow() start = datetime.utcnow()
deleted = delete_failed_notifications_created_more_than_a_week_ago() deleted = delete_notifications_created_more_than_a_week_ago('failed')
current_app.logger.info( current_app.logger.info(
"Delete job started {} finished {} deleted {} failed notifications".format( "Delete job started {} finished {} deleted {} failed notifications".format(
start, start,
@@ -185,29 +181,34 @@ def process_job(job_id):
job.processing_started = start job.processing_started = start
job.processing_finished = finished job.processing_finished = finished
dao_update_job(job) dao_update_job(job)
remove_job.apply_async((str(job_id),), queue='remove-job')
current_app.logger.info( current_app.logger.info(
"Job {} created at {} started at {} finished at {}".format(job_id, job.created_at, start, finished) "Job {} created at {} started at {} finished at {}".format(job_id, job.created_at, start, finished)
) )
@notify_celery.task(name="remove-job")
def remove_job(job_id):
job = dao_get_job_by_id(job_id)
s3.remove_job_from_s3(job.bucket_name, job_id)
current_app.logger.info("Job {} has been removed from s3.".format(job_id))
@notify_celery.task(name="send-sms") @notify_celery.task(name="send-sms")
def send_sms(service_id, notification_id, encrypted_notification, created_at): def send_sms(service_id, notification_id, encrypted_notification, created_at):
notification = encryption.decrypt(encrypted_notification) notification = encryption.decrypt(encrypted_notification)
service = dao_fetch_service_by_id(service_id) service = dao_fetch_service_by_id(service_id)
client = firetext_client client = firetext_client
restricted = False
if not service_allowed_to_send_to(notification['to'], service):
current_app.logger.info(
"SMS {} failed as restricted service".format(notification_id)
)
restricted = True
try: try:
status = 'sent'
can_send = True
if not allowed_send_to_number(service, notification['to']):
current_app.logger.info(
"SMS {} failed as restricted service".format(notification_id)
)
status = 'failed'
can_send = False
sent_at = datetime.utcnow() sent_at = datetime.utcnow()
notification_db_object = Notification( notification_db_object = Notification(
id=notification_id, id=notification_id,
@@ -215,7 +216,7 @@ def send_sms(service_id, notification_id, encrypted_notification, created_at):
to=notification['to'], to=notification['to'],
service_id=service_id, service_id=service_id,
job_id=notification.get('job', None), job_id=notification.get('job', None),
status=status, status='failed' if restricted else 'sent',
created_at=datetime.strptime(created_at, DATETIME_FORMAT), created_at=datetime.strptime(created_at, DATETIME_FORMAT),
sent_at=sent_at, sent_at=sent_at,
sent_by=client.get_name() sent_by=client.get_name()
@@ -223,30 +224,32 @@ def send_sms(service_id, notification_id, encrypted_notification, created_at):
dao_create_notification(notification_db_object, TEMPLATE_TYPE_SMS) dao_create_notification(notification_db_object, TEMPLATE_TYPE_SMS)
if can_send: if restricted:
try: return
template = Template(
dao_get_template_by_id(notification['template']).__dict__,
values=notification.get('personalisation', {}),
prefix=service.name
)
client.send_sms( try:
to=validate_and_format_phone_number(notification['to']), template = Template(
content=template.replaced, dao_get_template_by_id(notification['template']).__dict__,
reference=str(notification_id) values=notification.get('personalisation', {}),
) prefix=service.name
except FiretextClientException as e:
current_app.logger.error(
"SMS notification {} failed".format(notification_id)
)
current_app.logger.exception(e)
notification_db_object.status = 'failed'
dao_update_notification(notification_db_object)
current_app.logger.info(
"SMS {} created at {} sent at {}".format(notification_id, created_at, sent_at)
) )
client.send_sms(
to=validate_and_format_phone_number(notification['to']),
content=template.replaced,
reference=str(notification_id)
)
except FiretextClientException as e:
current_app.logger.error(
"SMS notification {} failed".format(notification_id)
)
current_app.logger.exception(e)
notification_db_object.status = 'failed'
dao_update_notification(notification_db_object)
current_app.logger.info(
"SMS {} created at {} sent at {}".format(notification_id, created_at, sent_at)
)
except SQLAlchemyError as e: except SQLAlchemyError as e:
current_app.logger.debug(e) current_app.logger.debug(e)
@@ -254,21 +257,18 @@ def send_sms(service_id, notification_id, encrypted_notification, created_at):
@notify_celery.task(name="send-email") @notify_celery.task(name="send-email")
def send_email(service_id, notification_id, subject, from_address, encrypted_notification, created_at): def send_email(service_id, notification_id, subject, from_address, encrypted_notification, created_at):
notification = encryption.decrypt(encrypted_notification) notification = encryption.decrypt(encrypted_notification)
client = aws_ses_client
service = dao_fetch_service_by_id(service_id) service = dao_fetch_service_by_id(service_id)
client = aws_ses_client restricted = False
if not service_allowed_to_send_to(notification['to'], service):
current_app.logger.info(
"Email {} failed as restricted service".format(notification_id)
)
restricted = True
try: try:
status = 'sent'
can_send = True
if not allowed_send_to_email(service, notification['to']):
current_app.logger.info(
"Email {} failed as restricted service".format(notification_id)
)
status = 'failed'
can_send = False
sent_at = datetime.utcnow() sent_at = datetime.utcnow()
notification_db_object = Notification( notification_db_object = Notification(
id=notification_id, id=notification_id,
@@ -276,36 +276,38 @@ def send_email(service_id, notification_id, subject, from_address, encrypted_not
to=notification['to'], to=notification['to'],
service_id=service_id, service_id=service_id,
job_id=notification.get('job', None), job_id=notification.get('job', None),
status=status, status='failed' if restricted else 'sent',
created_at=datetime.strptime(created_at, DATETIME_FORMAT), created_at=datetime.strptime(created_at, DATETIME_FORMAT),
sent_at=sent_at, sent_at=sent_at,
sent_by=client.get_name() sent_by=client.get_name()
) )
dao_create_notification(notification_db_object, TEMPLATE_TYPE_EMAIL) dao_create_notification(notification_db_object, TEMPLATE_TYPE_EMAIL)
if can_send: if restricted:
try: return
template = Template(
dao_get_template_by_id(notification['template']).__dict__,
values=notification.get('personalisation', {})
)
reference = client.send_email( try:
from_address, template = Template(
notification['to'], dao_get_template_by_id(notification['template']).__dict__,
subject, values=notification.get('personalisation', {})
body=template.replaced,
html_body=template.as_HTML_email,
)
update_notification_reference_by_id(notification_id, reference)
except AwsSesClientException as e:
current_app.logger.exception(e)
notification_db_object.status = 'failed'
dao_update_notification(notification_db_object)
current_app.logger.info(
"Email {} created at {} sent at {}".format(notification_id, created_at, sent_at)
) )
reference = client.send_email(
from_address,
notification['to'],
subject,
body=template.replaced,
html_body=template.as_HTML_email,
)
update_notification_reference_by_id(notification_id, reference)
except AwsSesClientException as e:
current_app.logger.exception(e)
notification_db_object.status = 'failed'
dao_update_notification(notification_db_object)
current_app.logger.info(
"Email {} created at {} sent at {}".format(notification_id, created_at, sent_at)
)
except SQLAlchemyError as e: except SQLAlchemyError as e:
current_app.logger.debug(e) current_app.logger.debug(e)
@@ -423,3 +425,16 @@ def email_registration_verification(encrypted_verification_message):
url=verification_message['url'])) url=verification_message['url']))
except AwsSesClientException as e: except AwsSesClientException as e:
current_app.logger.exception(e) current_app.logger.exception(e)
def service_allowed_to_send_to(recipient, service):
if not service.restricted:
return True
return allowed_to_send_to(
recipient,
itertools.chain.from_iterable(
[user.mobile_number, user.email_address] for user in service.users
)
)
+81 -43
View File
@@ -1,4 +1,8 @@
from sqlalchemy import desc from sqlalchemy import (
desc,
func
)
from datetime import ( from datetime import (
datetime, datetime,
timedelta, timedelta,
@@ -6,6 +10,7 @@ from datetime import (
) )
from flask import current_app from flask import current_app
from werkzeug.datastructures import MultiDict
from app import db from app import db
from app.models import ( from app.models import (
@@ -24,6 +29,23 @@ from app.clients import (
STATISTICS_REQUESTED STATISTICS_REQUESTED
) )
from functools import wraps
def transactional(func):
@wraps(func)
def commit_or_rollback(*args, **kwargs):
from flask import current_app
from app import db
try:
func(*args, **kwargs)
db.session.commit()
except Exception as e:
current_app.logger.error(e)
db.session.rollback()
raise
return commit_or_rollback
def dao_get_notification_statistics_for_service(service_id): def dao_get_notification_statistics_for_service(service_id):
return NotificationStatistics.query.filter_by( return NotificationStatistics.query.filter_by(
@@ -38,46 +60,56 @@ def dao_get_notification_statistics_for_service_and_day(service_id, day):
).order_by(desc(NotificationStatistics.day)).first() ).order_by(desc(NotificationStatistics.day)).first()
def dao_get_template_statistics_for_service(service_id, limit_days=None):
filter = [TemplateStatistics.service_id == service_id]
if limit_days:
latest_stat = TemplateStatistics.query.filter_by(service_id=service_id).order_by(
desc(TemplateStatistics.day)).limit(1).first()
if latest_stat:
last_date_to_fetch = latest_stat.day - timedelta(days=limit_days)
else:
last_date_to_fetch = date.today() - timedelta(days=limit_days)
filter.append(TemplateStatistics.day > last_date_to_fetch)
return TemplateStatistics.query.filter(*filter).order_by(
desc(TemplateStatistics.day)).join(Template).order_by(func.lower(Template.name)).all()
@transactional
def dao_create_notification(notification, notification_type): def dao_create_notification(notification, notification_type):
try: if notification.job_id:
if notification.job_id: db.session.query(Job).filter_by(
db.session.query(Job).filter_by( id=notification.job_id
id=notification.job_id ).update({
).update({ Job.notifications_sent: Job.notifications_sent + 1,
Job.notifications_sent: Job.notifications_sent + 1, Job.updated_at: datetime.utcnow()
Job.updated_at: datetime.utcnow() })
})
update_count = db.session.query(NotificationStatistics).filter_by( update_count = db.session.query(NotificationStatistics).filter_by(
day=notification.created_at.strftime('%Y-%m-%d'),
service_id=notification.service_id
).update(update_query(notification_type, 'requested'))
if update_count == 0:
stats = NotificationStatistics(
day=notification.created_at.strftime('%Y-%m-%d'), day=notification.created_at.strftime('%Y-%m-%d'),
service_id=notification.service_id
).update(update_query(notification_type, 'requested'))
if update_count == 0:
stats = NotificationStatistics(
day=notification.created_at.strftime('%Y-%m-%d'),
service_id=notification.service_id,
sms_requested=1 if notification_type == TEMPLATE_TYPE_SMS else 0,
emails_requested=1 if notification_type == TEMPLATE_TYPE_EMAIL else 0
)
db.session.add(stats)
update_count = db.session.query(TemplateStatistics).filter_by(
day=date.today(),
service_id=notification.service_id, service_id=notification.service_id,
template_id=notification.template_id sms_requested=1 if notification_type == TEMPLATE_TYPE_SMS else 0,
).update({'usage_count': TemplateStatistics.usage_count + 1}) emails_requested=1 if notification_type == TEMPLATE_TYPE_EMAIL else 0
)
db.session.add(stats)
if update_count == 0: update_count = db.session.query(TemplateStatistics).filter_by(
template_stats = TemplateStatistics(template_id=notification.template_id, day=date.today(),
service_id=notification.service_id) service_id=notification.service_id,
db.session.add(template_stats) template_id=notification.template_id
).update({'usage_count': TemplateStatistics.usage_count + 1})
db.session.add(notification) if update_count == 0:
db.session.commit() template_stats = TemplateStatistics(template_id=notification.template_id,
except: service_id=notification.service_id)
db.session.rollback() db.session.add(template_stats)
raise
db.session.add(notification)
def update_query(notification_type, status): def update_query(notification_type, status):
@@ -191,26 +223,32 @@ def get_notifications_for_service(service_id, filter_dict=None, page=1):
def filter_query(query, filter_dict=None): def filter_query(query, filter_dict=None):
if filter_dict and 'status' in filter_dict: if filter_dict is None:
query = query.filter_by(status=filter_dict['status']) filter_dict = MultiDict()
if filter_dict and 'template_type' in filter_dict: else:
query = query.join(Template).filter(Template.template_type == filter_dict['template_type']) filter_dict = MultiDict(filter_dict)
statuses = filter_dict.getlist('status') if 'status' in filter_dict else None
if statuses:
query = query.filter(Notification.status.in_(statuses))
template_types = filter_dict.getlist('template_type') if 'template_type' in filter_dict else None
if template_types:
query = query.join(Template).filter(Template.template_type.in_(template_types))
return query return query
def delete_successful_notifications_created_more_than_a_day_ago(): def delete_notifications_created_more_than_a_day_ago(status):
deleted = db.session.query(Notification).filter( deleted = db.session.query(Notification).filter(
Notification.created_at < datetime.utcnow() - timedelta(days=1), Notification.created_at < datetime.utcnow() - timedelta(days=1),
Notification.status == 'sent' Notification.status == status
).delete() ).delete()
db.session.commit() db.session.commit()
return deleted return deleted
def delete_failed_notifications_created_more_than_a_week_ago(): def delete_notifications_created_more_than_a_week_ago(status):
deleted = db.session.query(Notification).filter( deleted = db.session.query(Notification).filter(
Notification.created_at < datetime.utcnow() - timedelta(days=7), Notification.created_at < datetime.utcnow() - timedelta(days=7),
Notification.status == 'failed' Notification.status == status
).delete() ).delete()
db.session.commit() db.session.commit()
return deleted return deleted
+4 -2
View File
@@ -32,8 +32,10 @@ class PermissionDAO(DAOClass):
class Meta: class Meta:
model = Permission model = Permission
def get_query(self, filter_by_dict={}): def get_query(self, filter_by_dict=None):
if isinstance(filter_by_dict, dict): if filter_by_dict is None:
filter_by_dict = MultiDict()
else:
filter_by_dict = MultiDict(filter_by_dict) filter_by_dict = MultiDict(filter_by_dict)
query = self.Meta.model.query query = self.Meta.model.query
if 'id' in filter_by_dict: if 'id' in filter_by_dict:
+2
View File
@@ -37,6 +37,8 @@ def get_job_by_service_and_job_id(service_id, job_id):
def get_jobs_by_service(service_id): def get_jobs_by_service(service_id):
jobs = dao_get_jobs_by_service_id(service_id) jobs = dao_get_jobs_by_service_id(service_id)
data, errors = job_schema.dump(jobs, many=True) data, errors = job_schema.dump(jobs, many=True)
if errors:
return jsonify(result="error", message=errors), 400
return jsonify(data=data) return jsonify(data=data)
+1 -1
View File
@@ -357,7 +357,7 @@ class TemplateStatistics(db.Model):
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 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'), index=True, unique=False, nullable=False) service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, unique=False, nullable=False)
service = db.relationship('Service', backref=db.backref('template_statics', lazy='dynamic')) service = db.relationship('Service', backref=db.backref('template_statistics', lazy='dynamic'))
template_id = db.Column(db.BigInteger, db.ForeignKey('templates.id'), index=True, nullable=False, unique=False) template_id = db.Column(db.BigInteger, db.ForeignKey('templates.id'), index=True, nullable=False, unique=False)
template = db.relationship('Template') template = db.relationship('Template')
usage_count = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=1) usage_count = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=1)
+14 -8
View File
@@ -1,5 +1,6 @@
from datetime import datetime from datetime import datetime
import itertools
from flask import ( from flask import (
Blueprint, Blueprint,
jsonify, jsonify,
@@ -8,7 +9,7 @@ from flask import (
url_for, url_for,
json json
) )
from utils.recipients import allowed_to_send_to, first_column_heading
from utils.template import Template from utils.template import Template
from app.clients.email.aws_ses import AwsSesResponses from app.clients.email.aws_ses import AwsSesResponses
from app import api_user, encryption, create_uuid, DATETIME_FORMAT, DATE_FORMAT from app import api_user, encryption, create_uuid, DATETIME_FORMAT, DATE_FORMAT
@@ -29,7 +30,6 @@ from app.schemas import (
notifications_filter_schema notifications_filter_schema
) )
from app.celery.tasks import send_sms, send_email from app.celery.tasks import send_sms, send_email
from app.validation import allowed_send_to_number, allowed_send_to_email
notifications = Blueprint('notifications', __name__) notifications = Blueprint('notifications', __name__)
@@ -327,12 +327,21 @@ def send_notification(notification_type):
} }
), 400 ), 400
if service.restricted and not allowed_to_send_to(
notification['to'],
itertools.chain.from_iterable(
[user.mobile_number, user.email_address] for user in service.users
)
):
return jsonify(
result="error", message={
'to': ['Invalid {} for restricted service'.format(first_column_heading[notification_type])]
}
), 400
notification_id = create_uuid() notification_id = create_uuid()
if notification_type == 'sms': if notification_type == 'sms':
if not allowed_send_to_number(service, notification['to']):
return jsonify(
result="error", message={'to': ['Invalid phone number for restricted service']}), 400
send_sms.apply_async(( send_sms.apply_async((
service_id, service_id,
notification_id, notification_id,
@@ -340,9 +349,6 @@ def send_notification(notification_type):
datetime.utcnow().strftime(DATETIME_FORMAT) datetime.utcnow().strftime(DATETIME_FORMAT)
), queue='sms') ), queue='sms')
else: else:
if not allowed_send_to_email(service, notification['to']):
return jsonify(
result="error", message={'to': ['Email address not permitted for restricted service']}), 400
send_email.apply_async(( send_email.apply_async((
service_id, service_id,
notification_id, notification_id,
+55 -9
View File
@@ -1,15 +1,27 @@
from flask_marshmallow.fields import fields from flask_marshmallow.fields import fields
from . import ma
from . import models from marshmallow import (
from app.dao.permissions_dao import permission_dao post_load,
from marshmallow import (post_load, ValidationError, validates, validates_schema) ValidationError,
validates,
validates_schema,
pre_load
)
from marshmallow_sqlalchemy import field_for from marshmallow_sqlalchemy import field_for
from utils.recipients import ( from utils.recipients import (
validate_email_address, InvalidEmailError, validate_email_address,
validate_phone_number, InvalidPhoneError, InvalidEmailError,
validate_phone_number,
InvalidPhoneError,
validate_and_format_phone_number validate_and_format_phone_number
) )
from app import ma
from app import models
from app.dao.permissions_dao import permission_dao
# TODO I think marshmallow provides a better integration and error handling. # TODO I think marshmallow provides a better integration and error handling.
# Would be better to replace functionality in dao with the marshmallow supported # Would be better to replace functionality in dao with the marshmallow supported
@@ -19,7 +31,7 @@ from utils.recipients import (
class BaseSchema(ma.ModelSchema): class BaseSchema(ma.ModelSchema):
def __init__(self, *args, load_json=False, **kwargs): def __init__(self, load_json=False, *args, **kwargs):
self.load_json = load_json self.load_json = load_json
super(BaseSchema, self).__init__(*args, **kwargs) super(BaseSchema, self).__init__(*args, **kwargs)
@@ -74,6 +86,11 @@ class ServiceSchema(BaseSchema):
exclude = ("updated_at", "created_at", "api_keys", "templates", "jobs", 'old_id') exclude = ("updated_at", "created_at", "api_keys", "templates", "jobs", 'old_id')
class NotificationModelSchema(BaseSchema):
class Meta:
model = models.Notification
class TemplateSchema(BaseSchema): class TemplateSchema(BaseSchema):
class Meta: class Meta:
model = models.Template model = models.Template
@@ -203,10 +220,29 @@ class EmailDataSchema(ma.Schema):
class NotificationsFilterSchema(ma.Schema): class NotificationsFilterSchema(ma.Schema):
template_type = field_for(models.Template, 'template_type', load_only=True, required=False) template_type = fields.Nested(TemplateSchema, only='template_type', many=True)
status = field_for(models.Notification, 'status', load_only=True, required=False) status = fields.Nested(NotificationModelSchema, only='status', many=True)
page = fields.Int(required=False) page = fields.Int(required=False)
@pre_load
def handle_multidict(self, in_data):
if isinstance(in_data, dict) and hasattr(in_data, 'getlist'):
out_data = dict([(k, in_data.get(k)) for k in in_data.keys()])
if 'template_type' in in_data:
out_data['template_type'] = [{'template_type': x} for x in in_data.getlist('template_type')]
if 'status' in in_data:
out_data['status'] = [{"status": x} for x in in_data.getlist('status')]
return out_data
@post_load
def convert_schema_object_to_field(self, in_data):
if 'template_type' in in_data:
in_data['template_type'] = [x.template_type for x in in_data['template_type']]
if 'status' in in_data:
in_data['status'] = [x.status for x in in_data['status']]
return in_data
@validates('page') @validates('page')
def validate_page(self, value): def validate_page(self, value):
try: try:
@@ -216,6 +252,15 @@ class NotificationsFilterSchema(ma.Schema):
except: except:
raise ValidationError("Not a positive integer") raise ValidationError("Not a positive integer")
class TemplateStatisticsSchema(BaseSchema):
template = fields.Nested(TemplateSchema, only=["id", "name", "template_type"], dump_only=True)
class Meta:
model = models.TemplateStatistics
user_schema = UserSchema() user_schema = UserSchema()
user_schema_load_json = UserSchema(load_json=True) user_schema_load_json = UserSchema(load_json=True)
service_schema = ServiceSchema() service_schema = ServiceSchema()
@@ -239,3 +284,4 @@ permission_schema = PermissionSchema()
email_data_request_schema = EmailDataSchema() email_data_request_schema = EmailDataSchema()
notifications_statistics_schema = NotificationsStatisticsSchema() notifications_statistics_schema = NotificationsStatisticsSchema()
notifications_filter_schema = NotificationsFilterSchema() notifications_filter_schema = NotificationsFilterSchema()
template_statistics_schema = TemplateStatisticsSchema()
View File
+36
View File
@@ -0,0 +1,36 @@
from flask import (
Blueprint,
jsonify,
request,
current_app
)
from app.dao.notifications_dao import dao_get_template_statistics_for_service
from app.schemas import template_statistics_schema
template_statistics = Blueprint('template-statistics',
__name__,
url_prefix='/service/<service_id>/template-statistics')
from app.errors import register_errors
register_errors(template_statistics)
@template_statistics.route('')
def get_template_statistics_for_service(service_id):
if request.args.get('limit_days'):
try:
limit_days = int(request.args['limit_days'])
except ValueError as e:
error = '{} is not an integer'.format(request.args['limit_days'])
current_app.logger.error(error)
return jsonify(result="error", message={'limit_days': [error]}), 400
else:
limit_days = None
stats = dao_get_template_statistics_for_service(service_id, limit_days=limit_days)
data, errors = template_statistics_schema.dump(stats, many=True)
if errors:
return jsonify(result="error", message=errors), 400
return jsonify(data=data)
-15
View File
@@ -1,15 +0,0 @@
from utils.recipients import format_phone_number, validate_phone_number
def allowed_send_to_number(service, to):
if service.restricted and format_phone_number(validate_phone_number(to)) not in [
format_phone_number(validate_phone_number(user.mobile_number)) for user in service.users
]:
return False
return True
def allowed_send_to_email(service, to):
if service.restricted and to not in [user.email_address for user in service.users]:
return False
return True
+1
View File
@@ -69,6 +69,7 @@ class Config(object):
Queue('email-code', Exchange('default'), routing_key='email-code'), Queue('email-code', Exchange('default'), routing_key='email-code'),
Queue('email-reset-password', Exchange('default'), routing_key='email-reset-password'), Queue('email-reset-password', Exchange('default'), routing_key='email-reset-password'),
Queue('process-job', Exchange('default'), routing_key='process-job'), Queue('process-job', Exchange('default'), routing_key='process-job'),
Queue('remove-job', Exchange('default'), routing_key='remove-job'),
Queue('bulk-sms', Exchange('default'), routing_key='bulk-sms'), Queue('bulk-sms', Exchange('default'), routing_key='bulk-sms'),
Queue('bulk-email', Exchange('default'), routing_key='bulk-email'), Queue('bulk-email', Exchange('default'), routing_key='bulk-email'),
Queue('email-invited-user', Exchange('default'), routing_key='email-invited-user'), Queue('email-invited-user', Exchange('default'), routing_key='email-invited-user'),
+1 -1
View File
@@ -21,4 +21,4 @@ monotonic==0.3
git+https://github.com/alphagov/notifications-python-client.git@0.2.6#egg=notifications-python-client==0.2.6 git+https://github.com/alphagov/notifications-python-client.git@0.2.6#egg=notifications-python-client==0.2.6
git+https://github.com/alphagov/notifications-utils.git@3.1.3#egg=notifications-utils==3.1.3 git+https://github.com/alphagov/notifications-utils.git@3.2.1#egg=notifications-utils==3.2.1
+55 -13
View File
@@ -49,16 +49,16 @@ def firetext_error():
mmg_error = {'Error': '40', 'Description': 'error'} mmg_error = {'Error': '40', 'Description': 'error'}
def test_should_call_delete_successful_notifications_in_task(notify_api, mocker): def test_should_call_delete_notifications_more_than_week_in_task(notify_api, mocker):
mocker.patch('app.celery.tasks.delete_successful_notifications_created_more_than_a_day_ago') mocker.patch('app.celery.tasks.delete_notifications_created_more_than_a_week_ago')
delete_successful_notifications() delete_successful_notifications()
assert tasks.delete_successful_notifications_created_more_than_a_day_ago.call_count == 1 assert tasks.delete_notifications_created_more_than_a_week_ago.call_count == 1
def test_should_call_delete_failed_notifications_in_task(notify_api, mocker): def test_should_call_delete_notifications_more_than_week_in_task(notify_api, mocker):
mocker.patch('app.celery.tasks.delete_failed_notifications_created_more_than_a_week_ago') mocker.patch('app.celery.tasks.delete_notifications_created_more_than_a_week_ago')
delete_failed_notifications() delete_failed_notifications()
assert tasks.delete_failed_notifications_created_more_than_a_week_ago.call_count == 1 assert tasks.delete_notifications_created_more_than_a_week_ago.call_count == 1
def test_should_call_delete_codes_on_delete_verify_codes_task(notify_api, mocker): def test_should_call_delete_codes_on_delete_verify_codes_task(notify_api, mocker):
@@ -74,7 +74,7 @@ def test_should_call_delete_invotations_on_delete_invitations_task(notify_api, m
@freeze_time("2016-01-01 11:09:00.061258") @freeze_time("2016-01-01 11:09:00.061258")
def test_should_process_sms_job(sample_job, mocker): def test_should_process_sms_job(sample_job, mocker, mock_celery_remove_job):
mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('sms')) mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('sms'))
mocker.patch('app.celery.tasks.send_sms.apply_async') mocker.patch('app.celery.tasks.send_sms.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted") mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
@@ -97,7 +97,10 @@ def test_should_process_sms_job(sample_job, mocker):
@freeze_time("2016-01-01 11:09:00.061258") @freeze_time("2016-01-01 11:09:00.061258")
def test_should_not_process_sms_job_if_would_exceed_send_limits(notify_db, notify_db_session, mocker): def test_should_not_process_sms_job_if_would_exceed_send_limits(notify_db,
notify_db_session,
mocker,
mock_celery_remove_job):
service = sample_service(notify_db, notify_db_session, limit=9) service = sample_service(notify_db, notify_db_session, limit=9)
job = sample_job(notify_db, notify_db_session, service=service, notification_count=10) job = sample_job(notify_db, notify_db_session, service=service, notification_count=10)
@@ -112,9 +115,13 @@ def test_should_not_process_sms_job_if_would_exceed_send_limits(notify_db, notif
job = jobs_dao.dao_get_job_by_id(job.id) job = jobs_dao.dao_get_job_by_id(job.id)
assert job.status == 'sending limits exceeded' assert job.status == 'sending limits exceeded'
tasks.send_sms.apply_async.assert_not_called() tasks.send_sms.apply_async.assert_not_called()
mock_celery_remove_job.assert_not_called()
def test_should_not_process_sms_job_if_would_exceed_send_limits_inc_today(notify_db, notify_db_session, mocker): def test_should_not_process_sms_job_if_would_exceed_send_limits_inc_today(notify_db,
notify_db_session,
mocker,
mock_celery_remove_job):
service = sample_service(notify_db, notify_db_session, limit=1) service = sample_service(notify_db, notify_db_session, limit=1)
job = sample_job(notify_db, notify_db_session, service=service) job = sample_job(notify_db, notify_db_session, service=service)
@@ -131,6 +138,7 @@ def test_should_not_process_sms_job_if_would_exceed_send_limits_inc_today(notify
assert job.status == 'sending limits exceeded' assert job.status == 'sending limits exceeded'
s3.get_job_from_s3.assert_not_called() s3.get_job_from_s3.assert_not_called()
tasks.send_sms.apply_async.assert_not_called() tasks.send_sms.apply_async.assert_not_called()
mock_celery_remove_job.assert_not_called()
def test_should_not_process_email_job_if_would_exceed_send_limits_inc_today(notify_db, notify_db_session, mocker): def test_should_not_process_email_job_if_would_exceed_send_limits_inc_today(notify_db, notify_db_session, mocker):
@@ -173,7 +181,10 @@ def test_should_not_process_email_job_if_would_exceed_send_limits(notify_db, not
@freeze_time("2016-01-01 11:09:00.061258") @freeze_time("2016-01-01 11:09:00.061258")
def test_should_process_sms_job_if_exactly_on_send_limits(notify_db, notify_db_session, mocker): def test_should_process_sms_job_if_exactly_on_send_limits(notify_db,
notify_db_session,
mocker,
mock_celery_remove_job):
service = sample_service(notify_db, notify_db_session, limit=10) service = sample_service(notify_db, notify_db_session, limit=10)
template = sample_email_template(notify_db, notify_db_session, service=service) template = sample_email_template(notify_db, notify_db_session, service=service)
job = sample_job(notify_db, notify_db_session, service=service, template=template, notification_count=10) job = sample_job(notify_db, notify_db_session, service=service, template=template, notification_count=10)
@@ -197,9 +208,10 @@ def test_should_process_sms_job_if_exactly_on_send_limits(notify_db, notify_db_s
"2016-01-01T11:09:00.061258"), "2016-01-01T11:09:00.061258"),
queue="bulk-email" queue="bulk-email"
) )
mock_celery_remove_job.assert_called_once_with((str(job.id),), queue="remove-job")
def test_should_not_create_send_task_for_empty_file(sample_job, mocker): def test_should_not_create_send_task_for_empty_file(sample_job, mocker, mock_celery_remove_job):
mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('empty')) mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('empty'))
mocker.patch('app.celery.tasks.send_sms.apply_async') mocker.patch('app.celery.tasks.send_sms.apply_async')
@@ -212,7 +224,7 @@ def test_should_not_create_send_task_for_empty_file(sample_job, mocker):
@freeze_time("2016-01-01 11:09:00.061258") @freeze_time("2016-01-01 11:09:00.061258")
def test_should_process_email_job(sample_email_job, mocker): def test_should_process_email_job(sample_email_job, mocker, mock_celery_remove_job):
mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('email')) mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('email'))
mocker.patch('app.celery.tasks.send_email.apply_async') mocker.patch('app.celery.tasks.send_email.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted") mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
@@ -234,9 +246,13 @@ def test_should_process_email_job(sample_email_job, mocker):
) )
job = jobs_dao.dao_get_job_by_id(sample_email_job.id) job = jobs_dao.dao_get_job_by_id(sample_email_job.id)
assert job.status == 'finished' assert job.status == 'finished'
mock_celery_remove_job.assert_called_once_with((str(job.id),), queue="remove-job")
def test_should_process_all_sms_job(sample_job, sample_job_with_placeholdered_template, mocker): def test_should_process_all_sms_job(sample_job,
sample_job_with_placeholdered_template,
mocker,
mock_celery_remove_job):
mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('multiple_sms')) mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('multiple_sms'))
mocker.patch('app.celery.tasks.send_sms.apply_async') mocker.patch('app.celery.tasks.send_sms.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted") mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
@@ -403,6 +419,32 @@ def test_should_send_email_if_restricted_service_and_valid_email(notify_db, noti
) )
def test_should_not_send_email_if_restricted_service_and_invalid_email_address(notify_db, notify_db_session, mocker):
user = sample_user(notify_db, notify_db_session)
service = sample_service(notify_db, notify_db_session, user=user, restricted=True)
template = sample_template(
notify_db, notify_db_session, service=service, template_type='email', subject_line='Hello'
)
notification = {
"template": template.id,
"to": "test@example.com"
}
mocker.patch('app.encryption.decrypt', return_value=notification)
mocker.patch('app.aws_ses_client.send_email')
notification_id = uuid.uuid4()
now = datetime.utcnow()
send_sms(
service.id,
notification_id,
"encrypted-in-reality",
now.strftime(DATETIME_FORMAT)
)
aws_ses_client.send_email.assert_not_called()
def test_should_send_template_to_correct_sms_provider_and_persist_with_job_id(sample_job, mocker): def test_should_send_template_to_correct_sms_provider_and_persist_with_job_id(sample_job, mocker):
notification = { notification = {
"template": sample_job.template.id, "template": sample_job.template.id,
+5
View File
@@ -345,6 +345,11 @@ def mock_encryption(mocker):
return mocker.patch('app.encryption.encrypt', return_value="something_encrypted") return mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
@pytest.fixture(scope='function')
def mock_celery_remove_job(mocker):
return mocker.patch('app.celery.tasks.remove_job.apply_async')
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def sample_invited_user(notify_db, def sample_invited_user(notify_db,
notify_db_session, notify_db_session,
+169 -10
View File
@@ -23,12 +23,13 @@ from app.dao.notifications_dao import (
get_notification_for_job, get_notification_for_job,
get_notifications_for_job, get_notifications_for_job,
dao_get_notification_statistics_for_service, dao_get_notification_statistics_for_service,
delete_successful_notifications_created_more_than_a_day_ago, delete_notifications_created_more_than_a_day_ago,
delete_failed_notifications_created_more_than_a_week_ago, delete_notifications_created_more_than_a_week_ago,
dao_get_notification_statistics_for_service_and_day, dao_get_notification_statistics_for_service_and_day,
update_notification_status_by_id, update_notification_status_by_id,
update_notification_reference_by_id, update_notification_reference_by_id,
update_notification_status_by_reference update_notification_status_by_reference,
dao_get_template_statistics_for_service
) )
from tests.app.conftest import sample_job from tests.app.conftest import sample_job
@@ -700,21 +701,21 @@ def test_update_notification(sample_notification, sample_template):
assert notification_from_db.status == 'failed' assert notification_from_db.status == 'failed'
def test_should_delete_sent_notifications_after_one_day(notify_db, notify_db_session): def test_should_delete_notifications_after_one_day(notify_db, notify_db_session):
created_at = datetime.utcnow() - timedelta(hours=24) created_at = datetime.utcnow() - timedelta(hours=24)
sample_notification(notify_db, notify_db_session, created_at=created_at) sample_notification(notify_db, notify_db_session, created_at=created_at)
sample_notification(notify_db, notify_db_session, created_at=created_at) sample_notification(notify_db, notify_db_session, created_at=created_at)
assert len(Notification.query.all()) == 2 assert len(Notification.query.all()) == 2
delete_successful_notifications_created_more_than_a_day_ago() delete_notifications_created_more_than_a_day_ago('sent')
assert len(Notification.query.all()) == 0 assert len(Notification.query.all()) == 0
def test_should_delete_failed_notifications_after_seven_days(notify_db, notify_db_session): def test_should_delete_notifications_after_seven_days(notify_db, notify_db_session):
created_at = datetime.utcnow() - timedelta(hours=24 * 7) created_at = datetime.utcnow() - timedelta(hours=24 * 7)
sample_notification(notify_db, notify_db_session, created_at=created_at, status="failed") sample_notification(notify_db, notify_db_session, created_at=created_at, status="failed")
sample_notification(notify_db, notify_db_session, created_at=created_at, status="failed") sample_notification(notify_db, notify_db_session, created_at=created_at, status="failed")
assert len(Notification.query.all()) == 2 assert len(Notification.query.all()) == 2
delete_failed_notifications_created_more_than_a_week_ago() delete_notifications_created_more_than_a_week_ago('failed')
assert len(Notification.query.all()) == 0 assert len(Notification.query.all()) == 0
@@ -725,7 +726,7 @@ def test_should_not_delete_sent_notifications_before_one_day(notify_db, notify_d
sample_notification(notify_db, notify_db_session, created_at=valid, to_field="valid") sample_notification(notify_db, notify_db_session, created_at=valid, to_field="valid")
assert len(Notification.query.all()) == 2 assert len(Notification.query.all()) == 2
delete_successful_notifications_created_more_than_a_day_ago() delete_notifications_created_more_than_a_day_ago('sent')
assert len(Notification.query.all()) == 1 assert len(Notification.query.all()) == 1
assert Notification.query.first().to == 'valid' assert Notification.query.first().to == 'valid'
@@ -736,7 +737,7 @@ def test_should_not_delete_failed_notifications_before_seven_days(notify_db, not
sample_notification(notify_db, notify_db_session, created_at=expired, status="failed", to_field="expired") sample_notification(notify_db, notify_db_session, created_at=expired, status="failed", to_field="expired")
sample_notification(notify_db, notify_db_session, created_at=valid, status="failed", to_field="valid") sample_notification(notify_db, notify_db_session, created_at=valid, status="failed", to_field="valid")
assert len(Notification.query.all()) == 2 assert len(Notification.query.all()) == 2
delete_failed_notifications_created_more_than_a_week_ago() delete_notifications_created_more_than_a_week_ago('failed')
assert len(Notification.query.all()) == 1 assert len(Notification.query.all()) == 1
assert Notification.query.first().to == 'valid' assert Notification.query.first().to == 'valid'
@@ -886,10 +887,168 @@ def test_successful_notification_inserts_followed_by_failure_does_not_increment_
db.session.execute('DROP TABLE TEMPLATE_STATISTICS') db.session.execute('DROP TABLE TEMPLATE_STATISTICS')
dao_create_notification(failing_notification, sample_template.template_type) dao_create_notification(failing_notification, sample_template.template_type)
except Exception as e: except Exception as e:
# There should be no additional notification stats or counts # There should be no additional notification stats or counts
assert NotificationStatistics.query.count() == 1 assert NotificationStatistics.query.count() == 1
notication_stats = NotificationStatistics.query.filter( notication_stats = NotificationStatistics.query.filter(
NotificationStatistics.service_id == sample_template.service.id NotificationStatistics.service_id == sample_template.service.id
).first() ).first()
assert notication_stats.sms_requested == 3 assert notication_stats.sms_requested == 3
@freeze_time("2016-03-30")
def test_get_template_stats_for_service_returns_stats_in_reverse_date_order(sample_template, sample_job):
template_stats = dao_get_template_statistics_for_service(sample_template.service.id)
assert len(template_stats) == 0
data = {
'to': '+44709123456',
'job_id': sample_job.id,
'service': sample_template.service,
'service_id': sample_template.service.id,
'template': sample_template,
'template_id': sample_template.id,
'created_at': datetime.utcnow()
}
notification = Notification(**data)
dao_create_notification(notification, sample_template.template_type)
# move on one day
with freeze_time('2016-03-31'):
new_notification = Notification(**data)
dao_create_notification(new_notification, sample_template.template_type)
# move on one more day
with freeze_time('2016-04-01'):
new_notification = Notification(**data)
dao_create_notification(new_notification, sample_template.template_type)
template_stats = dao_get_template_statistics_for_service(sample_template.service_id)
assert len(template_stats) == 3
assert template_stats[0].day == date(2016, 4, 1)
assert template_stats[1].day == date(2016, 3, 31)
assert template_stats[2].day == date(2016, 3, 30)
@freeze_time('2016-04-09')
def test_get_template_stats_for_service_returns_stats_can_limit_number_of_days_returned(sample_template):
template_stats = dao_get_template_statistics_for_service(sample_template.service.id)
assert len(template_stats) == 0
# make 9 stats records from 1st to 9th April
for i in range(1, 10):
past_date = '2016-04-0{}'.format(i)
with freeze_time(past_date):
template_stats = TemplateStatistics(template_id=sample_template.id,
service_id=sample_template.service_id)
db.session.add(template_stats)
db.session.commit()
# Retrieve last week of stats
template_stats = dao_get_template_statistics_for_service(sample_template.service_id, limit_days=7)
assert len(template_stats) == 7
assert template_stats[0].day == date(2016, 4, 9)
assert template_stats[6].day == date(2016, 4, 3)
@freeze_time('2016-04-09')
def test_get_template_stats_for_service_returns_stats_returns_all_stats_if_no_limit(sample_template):
template_stats = dao_get_template_statistics_for_service(sample_template.service.id)
assert len(template_stats) == 0
# make 9 stats records from 1st to 9th April
for i in range(1, 10):
past_date = '2016-04-0{}'.format(i)
with freeze_time(past_date):
template_stats = TemplateStatistics(template_id=sample_template.id,
service_id=sample_template.service_id)
db.session.add(template_stats)
db.session.commit()
template_stats = dao_get_template_statistics_for_service(sample_template.service_id)
assert len(template_stats) == 9
assert template_stats[0].day == date(2016, 4, 9)
assert template_stats[8].day == date(2016, 4, 1)
@freeze_time('2016-04-30')
def test_get_template_stats_for_service_returns_results_from_first_day_with_data(sample_template):
template_stats = dao_get_template_statistics_for_service(sample_template.service.id)
assert len(template_stats) == 0
# make 9 stats records from 1st to 9th April - no data after 10th
for i in range(1, 10):
past_date = '2016-04-0{}'.format(i)
with freeze_time(past_date):
template_stats = TemplateStatistics(template_id=sample_template.id,
service_id=sample_template.service_id)
db.session.add(template_stats)
db.session.commit()
# Retrieve one day of stats - read date is 2016-04-30
template_stats = dao_get_template_statistics_for_service(sample_template.service_id, limit_days=1)
assert len(template_stats) == 1
assert template_stats[0].day == date(2016, 4, 9)
# Retrieve three days of stats
template_stats = dao_get_template_statistics_for_service(sample_template.service_id, limit_days=3)
assert len(template_stats) == 3
assert template_stats[0].day == date(2016, 4, 9)
assert template_stats[1].day == date(2016, 4, 8)
assert template_stats[2].day == date(2016, 4, 7)
# Retrieve nine days of stats
template_stats = dao_get_template_statistics_for_service(sample_template.service_id, limit_days=9)
assert len(template_stats) == 9
assert template_stats[0].day == date(2016, 4, 9)
assert template_stats[8].day == date(2016, 4, 1)
# Retrieve with no limit
template_stats = dao_get_template_statistics_for_service(sample_template.service_id)
assert len(template_stats) == 9
assert template_stats[0].day == date(2016, 4, 9)
assert template_stats[8].day == date(2016, 4, 1)
def test_get_template_stats_for_service_with_limit_if_no_records_returns_empty_list(sample_template):
template_stats = dao_get_template_statistics_for_service(sample_template.service.id, limit_days=7)
assert len(template_stats) == 0
def test_get_template_stats_for_service_for_day_returns_stats_in_template_name_order(sample_service, sample_job):
from app.dao.templates_dao import dao_create_template
from app.models import Template
template_stats = dao_get_template_statistics_for_service(sample_service.id)
assert len(template_stats) == 0
template_names = ['Aardvark', 'ant', 'zebra', 'walrus', 'Donkey', 'Komodo dragon']
for name in template_names:
data = {
'name': name,
'template_type': 'sms',
'content': 'blah',
'service': sample_service
}
template = Template(**data)
dao_create_template(template)
notification_data = {
'to': '+44709123456',
'job_id': sample_job.id,
'service': template.service,
'service_id': template.service.id,
'template': template,
'template_id': template.id,
'created_at': datetime.utcnow()
}
notification = Notification(**notification_data)
dao_create_notification(notification, template.template_type)
template_stats = dao_get_template_statistics_for_service(template.service.id)
for i, name in enumerate(sorted(template_names, key=str.lower)):
assert template_stats[i].template.name == name
+104 -20
View File
@@ -230,28 +230,34 @@ def test_should_reject_invalid_page_param(notify_api, sample_email_template):
def test_should_return_pagination_links(notify_api, notify_db, notify_db_session, sample_email_template): def test_should_return_pagination_links(notify_api, notify_db, notify_db_session, sample_email_template):
with notify_api.test_request_context(): with notify_api.test_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
notify_api.config['PAGE_SIZE'] = 1 # Effectively mocking page size
original_page_size = notify_api.config['PAGE_SIZE']
try:
notify_api.config['PAGE_SIZE'] = 1
create_sample_notification(notify_db, notify_db_session, sample_email_template.service) create_sample_notification(notify_db, notify_db_session, sample_email_template.service)
notification_2 = create_sample_notification(notify_db, notify_db_session, sample_email_template.service) notification_2 = create_sample_notification(notify_db, notify_db_session, sample_email_template.service)
create_sample_notification(notify_db, notify_db_session, sample_email_template.service) create_sample_notification(notify_db, notify_db_session, sample_email_template.service)
auth_header = create_authorization_header( auth_header = create_authorization_header(
service_id=sample_email_template.service_id, service_id=sample_email_template.service_id,
path='/notifications', path='/notifications',
method='GET') method='GET')
response = client.get( response = client.get(
'/notifications?page=2', '/notifications?page=2',
headers=[auth_header]) headers=[auth_header])
notifications = json.loads(response.get_data(as_text=True)) notifications = json.loads(response.get_data(as_text=True))
assert len(notifications['notifications']) == 1 assert len(notifications['notifications']) == 1
assert notifications['links']['last'] == '/notifications?page=3' assert notifications['links']['last'] == '/notifications?page=3'
assert notifications['links']['prev'] == '/notifications?page=1' assert notifications['links']['prev'] == '/notifications?page=1'
assert notifications['links']['next'] == '/notifications?page=3' assert notifications['links']['next'] == '/notifications?page=3'
assert notifications['notifications'][0]['to'] == notification_2.to assert notifications['notifications'][0]['to'] == notification_2.to
assert response.status_code == 200 assert response.status_code == 200
finally:
notify_api.config['PAGE_SIZE'] = original_page_size
def test_get_all_notifications_returns_empty_list(notify_api, sample_api_key): def test_get_all_notifications_returns_empty_list(notify_api, sample_api_key):
@@ -301,6 +307,41 @@ def test_filter_by_template_type(notify_api, notify_db, notify_db_session, sampl
assert response.status_code == 200 assert response.status_code == 200
def test_filter_by_multiple_template_types(notify_api,
notify_db,
notify_db_session,
sample_template,
sample_email_template):
with notify_api.test_request_context():
with notify_api.test_client() as client:
notification_1 = create_sample_notification(
notify_db,
notify_db_session,
service=sample_email_template.service,
template=sample_template)
notification_2 = create_sample_notification(
notify_db,
notify_db_session,
service=sample_email_template.service,
template=sample_email_template)
auth_header = create_authorization_header(
service_id=sample_email_template.service_id,
path='/notifications',
method='GET')
response = client.get(
'/notifications?template_type=sms&template_type=email',
headers=[auth_header])
assert response.status_code == 200
notifications = json.loads(response.get_data(as_text=True))
assert len(notifications['notifications']) == 2
set(['sms', 'email']) == set(
[x['template']['template_type'] for x in notifications['notifications']])
def test_filter_by_status(notify_api, notify_db, notify_db_session, sample_email_template): def test_filter_by_status(notify_api, notify_db, notify_db_session, sample_email_template):
with notify_api.test_request_context(): with notify_api.test_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
@@ -309,8 +350,15 @@ def test_filter_by_status(notify_api, notify_db, notify_db_session, sample_email
notify_db, notify_db,
notify_db_session, notify_db_session,
service=sample_email_template.service, service=sample_email_template.service,
template=sample_email_template,
status="delivered") status="delivered")
notification_2 = create_sample_notification(
notify_db,
notify_db_session,
service=sample_email_template.service,
template=sample_email_template)
auth_header = create_authorization_header( auth_header = create_authorization_header(
service_id=sample_email_template.service_id, service_id=sample_email_template.service_id,
path='/notifications', path='/notifications',
@@ -326,6 +374,42 @@ def test_filter_by_status(notify_api, notify_db, notify_db_session, sample_email
assert response.status_code == 200 assert response.status_code == 200
def test_filter_by_multiple_statuss(notify_api,
notify_db,
notify_db_session,
sample_email_template):
with notify_api.test_request_context():
with notify_api.test_client() as client:
notification_1 = create_sample_notification(
notify_db,
notify_db_session,
service=sample_email_template.service,
template=sample_email_template,
status="delivered")
notification_2 = create_sample_notification(
notify_db,
notify_db_session,
service=sample_email_template.service,
template=sample_email_template)
auth_header = create_authorization_header(
service_id=sample_email_template.service_id,
path='/notifications',
method='GET')
response = client.get(
'/notifications?status=delivered&status=sent',
headers=[auth_header])
assert response.status_code == 200
notifications = json.loads(response.get_data(as_text=True))
assert len(notifications['notifications']) == 2
set(['delivered', 'sent']) == set(
[x['status'] for x in notifications['notifications']])
def test_filter_by_status_and_template_type(notify_api, def test_filter_by_status_and_template_type(notify_api,
notify_db, notify_db,
notify_db_session, notify_db_session,
@@ -795,7 +879,7 @@ def test_should_not_send_email_if_restricted_and_not_a_service_user(notify_api,
app.celery.tasks.send_email.apply_async.assert_not_called() app.celery.tasks.send_email.apply_async.assert_not_called()
assert response.status_code == 400 assert response.status_code == 400
assert 'Email address not permitted for restricted service' in json_resp['message']['to'] assert 'Invalid email address for restricted service' in json_resp['message']['to']
def test_should_not_send_email_for_job_if_restricted_and_not_a_service_user( def test_should_not_send_email_for_job_if_restricted_and_not_a_service_user(
@@ -831,7 +915,7 @@ def test_should_not_send_email_for_job_if_restricted_and_not_a_service_user(
app.celery.tasks.send_email.apply_async.assert_not_called() app.celery.tasks.send_email.apply_async.assert_not_called()
assert response.status_code == 400 assert response.status_code == 400
assert 'Email address not permitted for restricted service' in json_resp['message']['to'] assert 'Invalid email address for restricted service' in json_resp['message']['to']
@freeze_time("2016-01-01 11:09:00.061258") @freeze_time("2016-01-01 11:09:00.061258")
+136
View File
@@ -0,0 +1,136 @@
import json
from freezegun import freeze_time
from app import db
from app.models import TemplateStatistics
from tests import create_authorization_header
@freeze_time('2016-04-09')
def test_get_template_statistics_for_service_for_last_week(notify_api, sample_template):
# make 9 stats records from 1st to 9th April
for i in range(1, 10):
past_date = '2016-04-0{}'.format(i)
with freeze_time(past_date):
template_stats = TemplateStatistics(template_id=sample_template.id,
service_id=sample_template.service_id)
db.session.add(template_stats)
db.session.commit()
with notify_api.test_request_context():
with notify_api.test_client() as client:
auth_header = create_authorization_header(
path='/service/{}/template-statistics'.format(sample_template.service_id),
method='GET'
)
response = client.get(
'/service/{}/template-statistics'.format(sample_template.service_id),
headers=[('Content-Type', 'application/json'), auth_header],
query_string={'limit_days': 7}
)
assert response.status_code == 200
json_resp = json.loads(response.get_data(as_text=True))
assert len(json_resp['data']) == 7
assert json_resp['data'][0]['day'] == '2016-04-09'
assert json_resp['data'][6]['day'] == '2016-04-03'
@freeze_time('2016-04-30')
def test_get_template_statistics_for_service_for_last_actual_week_with_data(notify_api, sample_template):
# make 9 stats records from 1st to 9th April
for i in range(1, 10):
past_date = '2016-04-0{}'.format(i)
with freeze_time(past_date):
template_stats = TemplateStatistics(template_id=sample_template.id,
service_id=sample_template.service_id)
db.session.add(template_stats)
db.session.commit()
with notify_api.test_request_context():
with notify_api.test_client() as client:
auth_header = create_authorization_header(
path='/service/{}/template-statistics'.format(sample_template.service_id),
method='GET'
)
# Date is frozen at 2016-04-30 and no data written since
response = client.get(
'/service/{}/template-statistics'.format(sample_template.service_id),
headers=[('Content-Type', 'application/json'), auth_header],
query_string={'limit_days': 7}
)
assert response.status_code == 200
json_resp = json.loads(response.get_data(as_text=True))
assert len(json_resp['data']) == 7
assert json_resp['data'][0]['day'] == '2016-04-09'
assert json_resp['data'][6]['day'] == '2016-04-03'
def test_get_all_template_statistics_for_service(notify_api, sample_template):
# make 9 stats records from 1st to 9th April
for i in range(1, 10):
past_date = '2016-04-0{}'.format(i)
with freeze_time(past_date):
template_stats = TemplateStatistics(template_id=sample_template.id,
service_id=sample_template.service_id)
db.session.add(template_stats)
db.session.commit()
with notify_api.test_request_context():
with notify_api.test_client() as client:
auth_header = create_authorization_header(
path='/service/{}/template-statistics'.format(sample_template.service_id),
method='GET'
)
response = client.get(
'/service/{}/template-statistics'.format(sample_template.service_id),
headers=[('Content-Type', 'application/json'), auth_header]
)
assert response.status_code == 200
json_resp = json.loads(response.get_data(as_text=True))
assert len(json_resp['data']) == 9
assert json_resp['data'][0]['day'] == '2016-04-09'
assert json_resp['data'][8]['day'] == '2016-04-01'
def test_get_all_template_statistics_with_bad_limit_arg_returns_400(notify_api, sample_template):
# make 9 stats records from 1st to 9th April
for i in range(1, 10):
past_date = '2016-04-0{}'.format(i)
with freeze_time(past_date):
template_stats = TemplateStatistics(template_id=sample_template.id,
service_id=sample_template.service_id)
db.session.add(template_stats)
db.session.commit()
with notify_api.test_request_context():
with notify_api.test_client() as client:
auth_header = create_authorization_header(
path='/service/{}/template-statistics'.format(sample_template.service_id),
method='GET'
)
response = client.get(
'/service/{}/template-statistics'.format(sample_template.service_id),
headers=[('Content-Type', 'application/json'), auth_header],
query_string={'limit_days': 'blurk'}
)
assert response.status_code == 400
json_resp = json.loads(response.get_data(as_text=True))
assert json_resp['result'] == 'error'
assert json_resp['message'] == {'limit_days': ['blurk is not an integer']}
-65
View File
@@ -1,65 +0,0 @@
from app.models import User, Service
from app.validation import allowed_send_to_number, allowed_send_to_email
def test_allowed_send_to_number_returns_true_for_restricted_service_with_same_number():
mobile_number = '07524609792'
service = _create_service_data(mobile_number)
assert allowed_send_to_number(service, mobile_number)
def test_allowed_send_to_number_returns_false_for_restricted_service_with_different_number():
mobile_number = '00447524609792'
service = _create_service_data(mobile_number)
assert not allowed_send_to_number(service, '+447344609793')
def test_allowed_send_to_number_returns_true_for_unrestricted_service_with_different_number():
mobile_number = '+447524609792'
service = _create_service_data(mobile_number, False)
assert allowed_send_to_number(service, '+447344609793')
def test_allowed_send_to_email__returns_true_for_restricted_service_with_same_email():
email = 'testing@it.gov.uk'
service = _create_service_data(email_address=email)
assert allowed_send_to_email(service, email)
def test_allowed_send_to_email__returns_false_for_restricted_service_with_different_email():
email = 'testing@it.gov.uk'
service = _create_service_data(email_address=email)
assert not allowed_send_to_email(service, 'another@it.gov.uk')
def test_allowed_send_to_email__returns_false_for_restricted_service_with_different_email():
email = 'testing@it.gov.uk'
service = _create_service_data(email_address=email)
assert not allowed_send_to_email(service, 'another@it.gov.uk')
def test_allowed_send_to_email__returns_true_for_unrestricted_service_with_different_email():
email = 'testing@it.gov.uk'
service = _create_service_data(email_address=email, restricted=False)
assert allowed_send_to_number(service, 'another@it.gov.uk')
def _create_service_data(mobile_number='+447524609792', restricted=True, email_address='test_user@it.gov.uk'):
usr = {
'name': 'Test User',
'email_address': email_address,
'password': 'password',
'mobile_number': mobile_number,
'state': 'active'
}
user = User(**usr)
data = {
'name': 'Test service',
'limit': 10,
'active': False,
'restricted': restricted,
'email_from': 'test_service@it.gov.uk'
}
service = Service(**data)
service.users = [user]
return service