mirror of
https://github.com/GSA/notifications-api.git
synced 2026-07-30 11:18:54 -04:00
Merge branch 'master' into template-query-to-not-limit-by-days
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
9
app/inbound_sms/inbound_sms_schemas.py
Normal file
9
app/inbound_sms/inbound_sms_schemas.py
Normal 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}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user