Merge branch 'master' into template-query-to-not-limit-by-days

This commit is contained in:
Martyn Inglis
2017-06-16 14:06:56 +01:00
36 changed files with 1897 additions and 386 deletions

View File

@@ -1,6 +1,10 @@
from boto3 import resource
from datetime import datetime, timedelta
from flask import current_app
import pytz
from boto3 import client, resource
FILE_LOCATION_STRUCTURE = 'service-{}-notify/{}.csv'
@@ -24,5 +28,51 @@ def get_job_from_s3(service_id, job_id):
def remove_job_from_s3(service_id, job_id):
bucket_name = current_app.config['CSV_UPLOAD_BUCKET_NAME']
file_location = FILE_LOCATION_STRUCTURE.format(service_id, job_id)
return remove_s3_object(bucket_name, file_location)
def get_s3_bucket_objects(bucket_name, subfolder='', older_than=7, limit_days=2):
boto_client = client('s3', current_app.config['AWS_REGION'])
paginator = boto_client.get_paginator('list_objects_v2')
page_iterator = paginator.paginate(
Bucket=bucket_name,
Prefix=subfolder
)
all_objects_in_bucket = []
for page in page_iterator:
all_objects_in_bucket.extend(page['Contents'])
return all_objects_in_bucket
def filter_s3_bucket_objects_within_date_range(bucket_objects, older_than=7, limit_days=2):
"""
S3 returns the Object['LastModified'] as an 'offset-aware' timestamp so the
date range filter must take this into account.
Additionally an additional Object is returned by S3 corresponding to the
container directory. This is redundant and should be removed.
"""
end_date = datetime.now(tz=pytz.utc) - timedelta(days=older_than)
start_date = end_date - timedelta(days=limit_days)
filtered_items = [item for item in bucket_objects if all([
not item['Key'].endswith('/'),
item['LastModified'] > start_date,
item['LastModified'] < end_date
])]
return filtered_items
def remove_s3_object(bucket_name, object_key):
obj = get_s3_object(bucket_name, object_key)
return obj.delete()
def remove_transformed_dvla_file(job_id):
bucket_name = current_app.config['DVLA_UPLOAD_BUCKET_NAME']
file_location = '{}-dvla-job.text'.format(job_id)
obj = get_s3_object(bucket_name, file_location)
return obj.delete()

View File

@@ -24,6 +24,7 @@ from app.dao.provider_details_dao import (
dao_toggle_sms_provider
)
from app.dao.users_dao import delete_codes_older_created_more_than_a_day_ago
from app.models import LETTER_TYPE
from app.notifications.process_notifications import send_notification_to_queue
from app.statsd_decorators import statsd
from app.celery.tasks import process_job
@@ -32,8 +33,8 @@ from app.config import QueueNames
@notify_celery.task(name="remove_csv_files")
@statsd(namespace="tasks")
def remove_csv_files():
jobs = dao_get_jobs_older_than_limited_by()
def remove_csv_files(job_types):
jobs = dao_get_jobs_older_than_limited_by(job_types=job_types)
for job in jobs:
s3.remove_job_from_s3(job.service_id, job.id)
current_app.logger.info("Job ID {} has been removed from s3.".format(job.id))
@@ -245,3 +246,38 @@ def delete_inbound_sms_older_than_seven_days():
except SQLAlchemyError as e:
current_app.logger.exception("Failed to delete inbound sms notifications")
raise
@notify_celery.task(name="remove_transformed_dvla_files")
@statsd(namespace="tasks")
def remove_transformed_dvla_files():
jobs = dao_get_jobs_older_than_limited_by(job_types=[LETTER_TYPE])
for job in jobs:
s3.remove_transformed_dvla_file(job.id)
current_app.logger.info("Transformed dvla file for job {} has been removed from s3.".format(job.id))
@notify_celery.task(name="delete_dvla_response_files")
@statsd(namespace="tasks")
def delete_dvla_response_files_older_than_seven_days():
try:
start = datetime.utcnow()
bucket_objects = s3.get_s3_bucket_objects(
current_app.config['DVLA_RESPONSE_BUCKET_NAME'],
'root/dispatch'
)
older_than_seven_days = s3.filter_s3_bucket_objects_within_date_range(bucket_objects)
for f in older_than_seven_days:
s3.remove_s3_object(current_app.config['DVLA_RESPONSE_BUCKET_NAME'], f['Key'])
current_app.logger.info(
"Delete dvla response files started {} finished {} deleted {} files".format(
start,
datetime.utcnow(),
len(older_than_seven_days)
)
)
except SQLAlchemyError as e:
current_app.logger.exception("Failed to delete dvla response files")
raise

View File

@@ -1,9 +1,13 @@
from datetime import timedelta
from celery.schedules import crontab
from kombu import Exchange, Queue
import os
from app.models import KEY_TYPE_NORMAL, KEY_TYPE_TEAM, KEY_TYPE_TEST
from celery.schedules import crontab
from kombu import Exchange, Queue
from app.models import (
EMAIL_TYPE, SMS_TYPE, LETTER_TYPE,
KEY_TYPE_NORMAL, KEY_TYPE_TEAM, KEY_TYPE_TEST
)
if os.environ.get('VCAP_SERVICES'):
# on cloudfoundry, config is a json blob in VCAP_SERVICES - unpack it, and populate
@@ -189,9 +193,26 @@ class Config(object):
'schedule': crontab(minute=0, hour=3),
'options': {'queue': QueueNames.PERIODIC}
},
'remove_csv_files': {
'remove_sms_email_jobs': {
'task': 'remove_csv_files',
'schedule': crontab(minute=0, hour=4),
'options': {'queue': QueueNames.PERIODIC},
'kwargs': {'job_types': [EMAIL_TYPE, SMS_TYPE]}
},
'remove_letter_jobs': {
'task': 'remove_csv_files',
'schedule': crontab(minute=20, hour=4),
'options': {'queue': QueueNames.PERIODIC},
'kwargs': {'job_types': [LETTER_TYPE]}
},
'remove_transformed_dvla_files': {
'task': 'remove_transformed_dvla_files',
'schedule': crontab(minute=40, hour=4),
'options': {'queue': QueueNames.PERIODIC}
},
'delete_dvla_response_files': {
'task': 'delete_dvla_response_files',
'schedule': crontab(minute=10, hour=5),
'options': {'queue': QueueNames.PERIODIC}
},
'timeout-job-statistics': {
@@ -239,6 +260,8 @@ class Config(object):
}
}
FREE_SMS_TIER_FRAGMENT_COUNT = 250000
######################
# Config overrides ###
@@ -248,6 +271,7 @@ class Development(Config):
SQLALCHEMY_ECHO = False
NOTIFY_EMAIL_DOMAIN = 'notify.tools'
CSV_UPLOAD_BUCKET_NAME = 'development-notifications-csv-upload'
DVLA_RESPONSE_BUCKET_NAME = 'notify.tools-ftp'
NOTIFY_ENVIRONMENT = 'development'
NOTIFICATION_QUEUE_PREFIX = 'development'
DEBUG = True
@@ -267,6 +291,7 @@ class Test(Config):
NOTIFY_ENVIRONMENT = 'test'
DEBUG = True
CSV_UPLOAD_BUCKET_NAME = 'test-notifications-csv-upload'
DVLA_RESPONSE_BUCKET_NAME = 'test.notify.com-ftp'
STATSD_ENABLED = True
STATSD_HOST = "localhost"
STATSD_PORT = 1000
@@ -299,6 +324,7 @@ class Preview(Config):
NOTIFY_EMAIL_DOMAIN = 'notify.works'
NOTIFY_ENVIRONMENT = 'preview'
CSV_UPLOAD_BUCKET_NAME = 'preview-notifications-csv-upload'
DVLA_RESPONSE_BUCKET_NAME = 'notify.works-ftp'
FROM_NUMBER = 'preview'
API_RATE_LIMIT_ENABLED = True
@@ -307,6 +333,7 @@ class Staging(Config):
NOTIFY_EMAIL_DOMAIN = 'staging-notify.works'
NOTIFY_ENVIRONMENT = 'staging'
CSV_UPLOAD_BUCKET_NAME = 'staging-notify-csv-upload'
DVLA_RESPONSE_BUCKET_NAME = 'staging-notify.works-ftp'
STATSD_ENABLED = True
FROM_NUMBER = 'stage'
API_RATE_LIMIT_ENABLED = True
@@ -316,6 +343,7 @@ class Live(Config):
NOTIFY_EMAIL_DOMAIN = 'notifications.service.gov.uk'
NOTIFY_ENVIRONMENT = 'live'
CSV_UPLOAD_BUCKET_NAME = 'live-notifications-csv-upload'
DVLA_RESPONSE_BUCKET_NAME = 'notifications.service.gov.uk-ftp'
STATSD_ENABLED = True
FROM_NUMBER = 'GOVUK'
FUNCTIONAL_TEST_PROVIDER_SERVICE_ID = '6c1d81bb-dae2-4ee9-80b0-89a4aae9f649'
@@ -333,6 +361,7 @@ class Sandbox(CloudFoundryConfig):
NOTIFY_EMAIL_DOMAIN = 'notify.works'
NOTIFY_ENVIRONMENT = 'sandbox'
CSV_UPLOAD_BUCKET_NAME = 'cf-sandbox-notifications-csv-upload'
DVLA_RESPONSE_BUCKET_NAME = 'notify.works-ftp'
FROM_NUMBER = 'sandbox'
REDIS_ENABLED = False

View File

@@ -47,3 +47,10 @@ def delete_inbound_sms_created_more_than_a_week_ago():
).delete(synchronize_session='fetch')
return deleted
def dao_get_inbound_sms_by_id(service_id, inbound_id):
return InboundSms.query.filter_by(
id=inbound_id,
service_id=service_id
).one()

View File

@@ -1,17 +1,15 @@
from datetime import datetime
from datetime import datetime, timedelta
from flask import current_app
from sqlalchemy import func, desc, asc, cast, Date as sql_date
from app import db
from app.dao import days_ago
from app.models import (Job,
Notification,
NotificationHistory,
Template,
JOB_STATUS_SCHEDULED,
JOB_STATUS_PENDING,
LETTER_TYPE, JobStatistics)
from app.models import (
Job, JobStatistics, Notification, NotificationHistory, Template,
JOB_STATUS_SCHEDULED, JOB_STATUS_PENDING,
LETTER_TYPE
)
from app.statsd_decorators import statsd
@@ -129,10 +127,14 @@ def dao_update_job_status(job_id, status):
db.session.commit()
def dao_get_jobs_older_than_limited_by(older_than=7, limit_days=2):
return Job.query.filter(
cast(Job.created_at, sql_date) < days_ago(older_than),
cast(Job.created_at, sql_date) >= days_ago(older_than + limit_days)
def dao_get_jobs_older_than_limited_by(job_types, older_than=7, limit_days=2):
end_date = datetime.utcnow() - timedelta(days=older_than)
start_date = end_date - timedelta(days=limit_days)
return Job.query.join(Template).filter(
Job.created_at < end_date,
Job.created_at >= start_date,
Template.template_type.in_(job_types)
).order_by(desc(Job.created_at)).all()
@@ -140,3 +142,56 @@ def dao_get_all_letter_jobs():
return db.session.query(Job).join(Job.template).filter(
Template.template_type == LETTER_TYPE
).order_by(desc(Job.created_at)).all()
@statsd(namespace="dao")
def dao_get_job_statistics_for_job(service_id, job_id):
query = Job.query.join(
JobStatistics, Job.id == JobStatistics.job_id
).filter(
Job.id == job_id,
Job.service_id == service_id
).add_columns(
JobStatistics.job_id,
Job.original_file_name,
Job.created_at,
Job.scheduled_for,
Job.template_id,
Job.template_version,
Job.job_status,
Job.service_id,
Job.notification_count,
JobStatistics.sent,
JobStatistics.delivered,
JobStatistics.failed
)
return query.one()
@statsd(namespace="dao")
def dao_get_job_stats_for_service(service_id, page=1, page_size=50, limit_days=None, statuses=None):
query = Job.query.join(
JobStatistics, Job.id == JobStatistics.job_id
).filter(
Job.service_id == service_id
).add_columns(
JobStatistics.job_id,
Job.original_file_name,
Job.created_at,
Job.scheduled_for,
Job.template_id,
Job.template_version,
Job.job_status,
Job.service_id,
Job.notification_count,
JobStatistics.sent,
JobStatistics.delivered,
JobStatistics.failed
)
if limit_days:
query = query.filter(Job.created_at >= days_ago(limit_days))
if statuses is not None and statuses != ['']:
query = query.filter(Job.job_status.in_(statuses))
query = query.order_by(Job.created_at.desc())
return query.paginate(page=page, per_page=page_size)

View File

@@ -1,5 +1,6 @@
from datetime import datetime, timedelta
from flask import current_app
from sqlalchemy import Float, Integer
from sqlalchemy import func, case, cast
from sqlalchemy import literal_column
@@ -11,7 +12,7 @@ from app.models import (NotificationHistory,
NOTIFICATION_STATUS_TYPES_BILLABLE,
KEY_TYPE_TEST,
SMS_TYPE,
EMAIL_TYPE)
EMAIL_TYPE, Service)
from app.statsd_decorators import statsd
from app.utils import get_london_month_from_utc_column
@@ -158,6 +159,8 @@ def rate_multiplier():
@statsd(namespace="dao")
def get_total_billable_units_for_sent_sms_notifications_in_date_range(start_date, end_date, service_id):
free_sms_limit = Service.free_sms_fragment_limit()
billable_units = 0
total_cost = 0.0
@@ -176,9 +179,15 @@ def get_total_billable_units_for_sent_sms_notifications_in_date_range(start_date
)
billable_units_by_rate_boundry = result.scalar()
if billable_units_by_rate_boundry:
billable_units += int(billable_units_by_rate_boundry)
total_cost += int(billable_units_by_rate_boundry) * rate_boundary['rate']
int_billable_units_by_rate_boundry = int(billable_units_by_rate_boundry)
if billable_units >= free_sms_limit:
total_cost += int_billable_units_by_rate_boundry * rate_boundary['rate']
elif billable_units + int_billable_units_by_rate_boundry > free_sms_limit:
remaining_free_allowance = abs(free_sms_limit - billable_units)
total_cost += ((int_billable_units_by_rate_boundry - remaining_free_allowance) * rate_boundary['rate'])
else:
total_cost += 0
billable_units += int_billable_units_by_rate_boundry
return billable_units, total_cost

View File

@@ -507,7 +507,7 @@ def dao_get_notifications_by_to_field(service_id, search_term, statuses=None):
if statuses:
filters.append(Notification.status.in_(statuses))
results = db.session.query(Notification).filter(*filters).all()
results = db.session.query(Notification).filter(*filters).order_by(desc(Notification.created_at)).all()
return results

View File

@@ -60,7 +60,10 @@ def timeout_job_counts(notifications_type, timeout_start):
).update({
sent: sent_count,
failed: failed_count,
delivered: delivered_count
delivered: delivered_count,
'sent': sent_count,
'delivered': delivered_count,
'failed': failed_count
}, synchronize_session=False)
return total_updated
@@ -87,11 +90,13 @@ def create_or_update_job_sending_statistics(notification):
@transactional
def __update_job_stats_sent_count(notification):
column = columns(notification.notification_type, 'sent')
new_column = 'sent'
return db.session.query(JobStatistics).filter_by(
job_id=notification.job_id,
).update({
column: column + 1
column: column + 1,
new_column: column + 1
})
@@ -102,7 +107,8 @@ def __insert_job_stats(notification):
emails_sent=1 if notification.notification_type == EMAIL_TYPE else 0,
sms_sent=1 if notification.notification_type == SMS_TYPE else 0,
letters_sent=1 if notification.notification_type == LETTER_TYPE else 0,
updated_at=datetime.utcnow()
updated_at=datetime.utcnow(),
sent=1
)
db.session.add(stats)
@@ -131,10 +137,12 @@ def columns(notification_type, status):
def update_job_stats_outcome_count(notification):
if notification.status in NOTIFICATION_STATUS_TYPES_FAILED:
column = columns(notification.notification_type, 'failed')
new_column = 'failed'
elif notification.status in [NOTIFICATION_DELIVERED,
NOTIFICATION_SENT] and notification.notification_type != LETTER_TYPE:
column = columns(notification.notification_type, 'delivered')
new_column = 'delivered'
else:
column = None
@@ -143,7 +151,8 @@ def update_job_stats_outcome_count(notification):
return db.session.query(JobStatistics).filter_by(
job_id=notification.job_id,
).update({
column: column + 1
column: column + 1,
new_column: column + 1
})
else:
return 0

View File

@@ -1,10 +1,11 @@
from flask import (
jsonify,
current_app
)
current_app,
json)
from sqlalchemy.exc import SQLAlchemyError, DataError
from sqlalchemy.orm.exc import NoResultFound
from marshmallow import ValidationError
from jsonschema import ValidationError as JsonSchemaValidationError
from app.authentication.auth import AuthError
from app.notifications import SendNotificationToQueueError
@@ -50,6 +51,11 @@ def register_errors(blueprint):
current_app.logger.error(error)
return jsonify(result='error', message=error.messages), 400
@blueprint.errorhandler(JsonSchemaValidationError)
def validation_error(error):
current_app.logger.exception(error)
return jsonify(json.loads(error.message)), 400
@blueprint.errorhandler(InvalidRequest)
def invalid_data(error):
response = jsonify(error.to_dict())

View File

@@ -0,0 +1,9 @@
get_inbound_sms_for_service_schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "schema for parameters allowed when searching for to field=",
"type": "object",
"properties": {
"phone_number": {"type": "string", "format": "phone_number"},
"limit": {"type": ["integer", "null"], "minimum": 1}
}
}

View File

@@ -1,34 +1,50 @@
from flask import (
Blueprint,
jsonify,
request
)
request,
current_app, json)
from jsonschema import ValidationError
from notifications_utils.recipients import validate_and_format_phone_number
from app.dao.inbound_sms_dao import dao_get_inbound_sms_for_service, dao_count_inbound_sms_for_service
from app.dao.inbound_sms_dao import (
dao_get_inbound_sms_for_service,
dao_count_inbound_sms_for_service,
dao_get_inbound_sms_by_id
)
from app.errors import register_errors
from app.schema_validation import validate
from app.inbound_sms.inbound_sms_schemas import get_inbound_sms_for_service_schema
inbound_sms = Blueprint(
'inbound_sms',
__name__,
url_prefix='/service/<service_id>/inbound-sms'
url_prefix='/service/<uuid:service_id>/inbound-sms'
)
register_errors(inbound_sms)
@inbound_sms.route('')
@inbound_sms.route('', methods=['POST', 'GET'])
def get_inbound_sms_for_service(service_id):
limit = request.args.get('limit')
user_number = request.args.get('user_number')
if user_number:
# we use this to normalise to an international phone number
user_number = validate_and_format_phone_number(user_number, international=True)
if request.method == 'GET':
limit = request.args.get('limit')
user_number = request.args.get('user_number')
results = dao_get_inbound_sms_for_service(service_id, limit, user_number)
if user_number:
# we use this to normalise to an international phone number
user_number = validate_and_format_phone_number(user_number, international=True)
return jsonify(data=[row.serialize() for row in results])
results = dao_get_inbound_sms_for_service(service_id, limit, user_number)
return jsonify(data=[row.serialize() for row in results])
else:
form = validate(request.get_json(), get_inbound_sms_for_service_schema)
results = dao_get_inbound_sms_for_service(service_id, form.get('limit'), form.get('phone_number'))
return jsonify(data=[row.serialize() for row in results])
@inbound_sms.route('/summary')
@@ -40,3 +56,10 @@ def get_inbound_sms_summary_for_service(service_id):
count=count,
most_recent=most_recent[0].created_at.isoformat() if most_recent else None
)
@inbound_sms.route('/<uuid:inbound_sms_id>', methods=['GET'])
def get_inbound_by_id(service_id, inbound_sms_id):
message = dao_get_inbound_sms_by_id(service_id, inbound_sms_id)
return jsonify(message.serialize()), 200

View File

@@ -5,14 +5,16 @@ from flask import (
current_app
)
from app import DATETIME_FORMAT
from app.dao.jobs_dao import (
dao_create_job,
dao_update_job,
dao_get_job_by_service_id_and_job_id,
dao_get_jobs_by_service_id,
dao_get_future_scheduled_job_by_id_and_service_id,
dao_get_notification_outcomes_for_job
)
dao_get_notification_outcomes_for_job,
dao_get_job_stats_for_service,
dao_get_job_statistics_for_job)
from app.dao.services_dao import (
dao_fetch_service_by_id
@@ -57,6 +59,13 @@ def get_job_by_service_and_job_id(service_id, job_id):
return jsonify(data=data)
@job_blueprint.route('/job-stats/<job_id>', methods=['GET'])
def get_job_stats_by_service_and_job_id(service_id, job_id):
statistic = dao_get_job_statistics_for_job(service_id=service_id, job_id=job_id)
return jsonify(_serialize_job_stats(statistic))
@job_blueprint.route('/<job_id>/cancel', methods=['POST'])
def cancel_job(service_id, job_id):
job = dao_get_future_scheduled_job_by_id_and_service_id(job_id, service_id)
@@ -117,6 +126,57 @@ def get_jobs_by_service(service_id):
return jsonify(**get_paginated_jobs(service_id, limit_days, statuses, page))
@job_blueprint.route('/job-stats', methods=['GET'])
def get_jobs_for_service(service_id):
if request.args.get('limit_days'):
try:
limit_days = int(request.args['limit_days'])
except ValueError:
errors = {'limit_days': ['{} is not an integer'.format(request.args['limit_days'])]}
raise InvalidRequest(errors, status_code=400)
else:
limit_days = None
statuses = _parse_statuses(request.args.get('statuses', ''))
page = int(request.args.get('page', 1))
pagination = dao_get_job_stats_for_service(service_id=service_id,
page=page,
page_size=current_app.config['PAGE_SIZE'],
limit_days=limit_days,
statuses=statuses)
return jsonify({
'data': [_serialize_job_stats(x) for x in pagination.items],
'page_size': pagination.per_page,
'total': pagination.total,
'links': pagination_links(
pagination,
'.get_jobs_by_service',
service_id=service_id
)
})
def _parse_statuses(statuses):
return [x.strip() for x in statuses.split(',')]
def _serialize_job_stats(stat):
return {
"job_id": stat.job_id,
"original_file_name": stat.original_file_name,
"created_at": stat.created_at.strftime(DATETIME_FORMAT),
"scheduled_for": stat.scheduled_for,
"template_id": stat.template_id,
"template_version": stat.template_version,
"job_status": stat.job_status,
"service_id": stat.service_id,
"requested": stat.notification_count,
"sent": stat.sent,
"delivered": stat.delivered,
"failed": stat.failed
}
@job_blueprint.route('', methods=['POST'])
def create_job(service_id):
service = dao_fetch_service_by_id(service_id)

View File

@@ -217,6 +217,10 @@ class Service(db.Model, Versioned):
self.can_send_letters = LETTER_TYPE in [p.permission for p in self.permissions]
self.can_send_international_sms = INTERNATIONAL_SMS_TYPE in [p.permission for p in self.permissions]
@staticmethod
def free_sms_fragment_limit():
return current_app.config['FREE_SMS_TIER_FRAGMENT_COUNT']
@classmethod
def from_json(cls, data):
"""
@@ -1122,6 +1126,9 @@ class JobStatistics(db.Model):
sms_failed = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
letters_sent = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
letters_failed = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
sent = db.Column(db.BigInteger, index=False, unique=False, nullable=True, default=0)
delivered = db.Column(db.BigInteger, index=False, unique=False, nullable=True, default=0)
failed = db.Column(db.BigInteger, index=False, unique=False, nullable=True, default=0)
created_at = db.Column(
db.DateTime,
index=False,
@@ -1165,7 +1172,7 @@ class InboundSms(db.Model):
user_number = db.Column(db.String, nullable=False) # the end user's number, that the msg was sent from
provider_date = db.Column(db.DateTime)
provider_reference = db.Column(db.String)
provider = db.Column(db.String, nullable=True)
provider = db.Column(db.String, nullable=False)
_content = db.Column('content', db.String, nullable=False)
@property

View File

@@ -11,6 +11,10 @@ from app.dao import (
templates_dao,
notifications_dao
)
from app.errors import (
register_errors,
InvalidRequest
)
from app.models import KEY_TYPE_TEAM, PRIORITY
from app.models import SMS_TYPE
from app.notifications.process_notifications import (
@@ -38,12 +42,6 @@ from notifications_utils.recipients import get_international_phone_info
notifications = Blueprint('notifications', __name__)
from app.errors import (
register_errors,
InvalidRequest
)
register_errors(notifications)

View File

@@ -25,9 +25,8 @@ from notifications_utils.recipients import (
from app import ma
from app import models
from app.models import ServicePermission, INTERNATIONAL_SMS_TYPE, SMS_TYPE, LETTER_TYPE, EMAIL_TYPE
from app.models import ServicePermission, INTERNATIONAL_SMS_TYPE, LETTER_TYPE
from app.dao.permissions_dao import permission_dao
from app.dao.service_permissions_dao import dao_fetch_service_permissions
from app.utils import get_template_instance
@@ -176,6 +175,7 @@ class ProviderDetailsHistorySchema(BaseSchema):
class ServiceSchema(BaseSchema):
free_sms_fragment_limit = fields.Method(method_name='get_free_sms_fragment_limit')
created_by = field_for(models.Service, 'created_by', required=True)
organisation = field_for(models.Service, 'organisation')
branding = field_for(models.Service, 'branding')
@@ -183,11 +183,15 @@ class ServiceSchema(BaseSchema):
permissions = fields.Method("service_permissions")
override_flag = False
def get_free_sms_fragment_limit(selfs, service):
return service.free_sms_fragment_limit()
def service_permissions(self, service):
return [p.permission for p in service.permissions]
class Meta:
model = models.Service
dump_only = ['free_sms_fragment_limit']
exclude = (
'updated_at',
'created_at',
@@ -256,6 +260,11 @@ class ServiceSchema(BaseSchema):
class DetailedServiceSchema(BaseSchema):
statistics = fields.Dict()
free_sms_fragment_limit = fields.Method(method_name='get_free_sms_fragment_limit')
def get_free_sms_fragment_limit(selfs, service):
return service.free_sms_fragment_limit()
class Meta:
model = models.Service
exclude = (

View File

@@ -59,6 +59,7 @@ from app.schemas import (
user_schema,
permission_schema,
notification_with_template_schema,
notification_with_personalisation_schema,
notifications_filter_schema,
detailed_service_schema
)
@@ -300,10 +301,23 @@ def get_all_notifications_for_service(service_id):
), 200
@service_blueprint.route('/<uuid:service_id>/notifications/<uuid:notification_id>', methods=['GET'])
def get_notification_for_service(service_id, notification_id):
notification = notifications_dao.get_notification_with_personalisation(
service_id,
notification_id,
key_type=None,
)
return jsonify(
notification_with_template_schema.dump(notification).data,
), 200
def search_for_notification_by_to_field(service_id, search_term, statuses):
results = notifications_dao.dao_get_notifications_by_to_field(service_id, search_term, statuses)
return jsonify(
notifications=notification_with_template_schema.dump(results, many=True).data
notifications=notification_with_personalisation_schema.dump(results, many=True).data
), 200

View File

@@ -206,7 +206,7 @@ def send_user_confirm_new_email(user_id):
personalisation={
'name': user_to_send_to.name,
'url': _create_confirmation_url(user=user_to_send_to, email_address=email['email']),
'feedback_url': current_app.config['ADMIN_BASE_URL'] + '/feedback'
'feedback_url': current_app.config['ADMIN_BASE_URL'] + '/support'
},
notification_type=EMAIL_TYPE,
api_key_id=None,
@@ -259,7 +259,7 @@ def send_already_registered_email(user_id):
personalisation={
'signin_url': current_app.config['ADMIN_BASE_URL'] + '/sign-in',
'forgot_password_url': current_app.config['ADMIN_BASE_URL'] + '/forgot-password',
'feedback_url': current_app.config['ADMIN_BASE_URL'] + '/feedback'
'feedback_url': current_app.config['ADMIN_BASE_URL'] + '/support'
},
notification_type=EMAIL_TYPE,
api_key_id=None,

View File

@@ -14,7 +14,7 @@ from app.v2.notifications.notification_schemas import get_notifications_request
def get_notification_by_id(id):
try:
casted_id = uuid.UUID(id)
except ValueError or AttributeError:
except (ValueError, AttributeError):
abort(404)
notification = notifications_dao.get_notification_with_personalisation(
authenticated_service.id, casted_id, key_type=None