2020-01-08 12:23:09 +00:00
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
from notifications_utils.letter_timings import (
|
|
|
|
|
|
CANCELLABLE_JOB_LETTER_STATUSES,
|
|
|
|
|
|
get_letter_timings,
|
|
|
|
|
|
letter_can_be_cancelled,
|
|
|
|
|
|
)
|
2020-01-21 12:23:30 +00:00
|
|
|
|
from notifications_utils.timezones import (
|
|
|
|
|
|
local_timezone,
|
|
|
|
|
|
utc_string_to_aware_gmt_datetime,
|
|
|
|
|
|
)
|
2020-01-08 12:23:09 +00:00
|
|
|
|
from werkzeug.utils import cached_property
|
|
|
|
|
|
|
2020-01-08 14:29:56 +00:00
|
|
|
|
from app.models import JSONModel, ModelList
|
2020-01-08 12:23:09 +00:00
|
|
|
|
from app.notify_client.job_api_client import job_api_client
|
|
|
|
|
|
from app.notify_client.notification_api_client import notification_api_client
|
|
|
|
|
|
from app.notify_client.service_api_client import service_api_client
|
|
|
|
|
|
from app.utils import set_status_filters
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Job(JSONModel):
|
|
|
|
|
|
|
|
|
|
|
|
ALLOWED_PROPERTIES = {
|
|
|
|
|
|
'id',
|
|
|
|
|
|
'service',
|
|
|
|
|
|
'template',
|
|
|
|
|
|
'template_version',
|
|
|
|
|
|
'original_file_name',
|
|
|
|
|
|
'created_at',
|
2020-01-21 12:23:30 +00:00
|
|
|
|
'processing_started',
|
2020-01-08 12:23:09 +00:00
|
|
|
|
'notification_count',
|
|
|
|
|
|
'created_by',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def from_id(cls, job_id, service_id):
|
|
|
|
|
|
return cls(job_api_client.get_job(service_id, job_id)['data'])
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def status(self):
|
2020-01-20 15:25:47 +00:00
|
|
|
|
return self._dict.get('job_status')
|
2020-01-08 12:23:09 +00:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def cancelled(self):
|
|
|
|
|
|
return self.status == 'cancelled'
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def scheduled(self):
|
|
|
|
|
|
return self.status == 'scheduled'
|
|
|
|
|
|
|
2020-01-17 10:01:39 +00:00
|
|
|
|
@property
|
|
|
|
|
|
def scheduled_for(self):
|
|
|
|
|
|
return self._dict.get('scheduled_for')
|
|
|
|
|
|
|
2020-02-04 15:36:55 +00:00
|
|
|
|
@property
|
|
|
|
|
|
def upload_type(self):
|
|
|
|
|
|
return self._dict.get('upload_type')
|
|
|
|
|
|
|
2020-01-16 16:58:26 +00:00
|
|
|
|
@property
|
|
|
|
|
|
def processing_started(self):
|
|
|
|
|
|
if not self._dict.get('processing_started'):
|
|
|
|
|
|
return None
|
2020-01-21 12:23:30 +00:00
|
|
|
|
return utc_string_to_aware_gmt_datetime(self._dict['processing_started'])
|
2020-01-16 16:58:26 +00:00
|
|
|
|
|
2020-01-09 15:54:42 +00:00
|
|
|
|
def _aggregate_statistics(self, *statuses):
|
2020-01-09 15:52:19 +00:00
|
|
|
|
return sum(
|
|
|
|
|
|
outcome['count'] for outcome in self._dict['statistics']
|
2020-01-09 15:54:42 +00:00
|
|
|
|
if not statuses or outcome['status'] in statuses
|
2020-01-09 15:52:19 +00:00
|
|
|
|
)
|
2020-01-08 12:23:09 +00:00
|
|
|
|
|
2020-01-09 15:54:42 +00:00
|
|
|
|
@property
|
|
|
|
|
|
def notifications_delivered(self):
|
|
|
|
|
|
return self._aggregate_statistics('delivered', 'sent')
|
|
|
|
|
|
|
2020-01-08 12:23:09 +00:00
|
|
|
|
@property
|
|
|
|
|
|
def notifications_failed(self):
|
2020-01-09 15:54:42 +00:00
|
|
|
|
return self._aggregate_statistics(
|
|
|
|
|
|
'failed', 'technical-failure', 'temporary-failure',
|
|
|
|
|
|
'permanent-failure', 'cancelled',
|
2020-01-09 15:52:19 +00:00
|
|
|
|
)
|
2020-01-08 16:32:08 +00:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def notifications_requested(self):
|
2020-01-09 15:54:42 +00:00
|
|
|
|
return self._aggregate_statistics()
|
2020-01-08 16:32:08 +00:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def notifications_sent(self):
|
|
|
|
|
|
return self.notifications_delivered + self.notifications_failed
|
2020-01-08 12:23:09 +00:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def notifications_sending(self):
|
|
|
|
|
|
if self.scheduled:
|
|
|
|
|
|
return 0
|
2020-01-08 16:32:08 +00:00
|
|
|
|
return self.notification_count - self.notifications_sent
|
2020-01-08 12:23:09 +00:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def notifications_created(self):
|
|
|
|
|
|
return notification_api_client.get_notification_count_for_job_id(
|
|
|
|
|
|
service_id=self.service, job_id=self.id
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def still_processing(self):
|
|
|
|
|
|
return (
|
2020-01-08 16:32:08 +00:00
|
|
|
|
self.percentage_complete < 100 and self.status != 'finished'
|
2020-01-08 12:23:09 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2020-01-08 16:23:43 +00:00
|
|
|
|
@cached_property
|
|
|
|
|
|
def finished_processing(self):
|
2020-01-16 15:40:49 +00:00
|
|
|
|
return self.notification_count == self.notifications_sent
|
2020-01-08 16:23:43 +00:00
|
|
|
|
|
2020-01-16 16:58:26 +00:00
|
|
|
|
@property
|
2020-01-21 14:07:23 +00:00
|
|
|
|
def awaiting_processing_or_recently_processed(self):
|
2020-01-16 16:58:26 +00:00
|
|
|
|
if not self.processing_started:
|
|
|
|
|
|
# Assume that if processing hasn’t started yet then the job
|
|
|
|
|
|
# must have been created recently enough to not have any
|
|
|
|
|
|
# notifications yet
|
|
|
|
|
|
return True
|
|
|
|
|
|
return (
|
2020-01-21 12:23:30 +00:00
|
|
|
|
datetime.utcnow().astimezone(local_timezone) - self.processing_started
|
2020-01-16 16:58:26 +00:00
|
|
|
|
).days < 1
|
|
|
|
|
|
|
2020-01-08 12:23:09 +00:00
|
|
|
|
@property
|
|
|
|
|
|
def template_id(self):
|
|
|
|
|
|
return self._dict['template']
|
|
|
|
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
|
|
def template(self):
|
|
|
|
|
|
return service_api_client.get_service_template(
|
|
|
|
|
|
service_id=self.service,
|
|
|
|
|
|
template_id=self.template_id,
|
|
|
|
|
|
version=self.template_version,
|
|
|
|
|
|
)['data']
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def template_type(self):
|
|
|
|
|
|
return self.template['template_type']
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def percentage_complete(self):
|
|
|
|
|
|
return self.notifications_requested / self.notification_count * 100
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def letter_job_can_be_cancelled(self):
|
|
|
|
|
|
|
|
|
|
|
|
if self.template['template_type'] != 'letter':
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
if any(self.uncancellable_notifications):
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
if not letter_can_be_cancelled(
|
2020-01-21 12:23:30 +00:00
|
|
|
|
'created',
|
|
|
|
|
|
utc_string_to_aware_gmt_datetime(self.created_at).replace(tzinfo=None)
|
2020-01-08 12:23:09 +00:00
|
|
|
|
):
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
|
|
def all_notifications(self):
|
|
|
|
|
|
return self.get_notifications(set_status_filters({}))['notifications']
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def uncancellable_notifications(self):
|
|
|
|
|
|
return (
|
|
|
|
|
|
n for n in self.all_notifications
|
|
|
|
|
|
if n['status'] not in CANCELLABLE_JOB_LETTER_STATUSES
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
|
|
def postage(self):
|
|
|
|
|
|
# There might be no notifications if the job has only just been
|
|
|
|
|
|
# created and the tasks haven't run yet
|
|
|
|
|
|
try:
|
|
|
|
|
|
return self.all_notifications[0]['postage']
|
|
|
|
|
|
except IndexError:
|
|
|
|
|
|
return self.template['postage']
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def letter_timings(self):
|
|
|
|
|
|
return get_letter_timings(self.created_at, postage=self.postage)
|
|
|
|
|
|
|
2020-01-08 14:29:56 +00:00
|
|
|
|
@property
|
|
|
|
|
|
def failure_rate(self):
|
|
|
|
|
|
if not self.notifications_delivered:
|
|
|
|
|
|
return 100 if self.notifications_failed else 0
|
|
|
|
|
|
return (
|
|
|
|
|
|
self.notifications_failed / (
|
|
|
|
|
|
self.notifications_failed + self.notifications_delivered
|
|
|
|
|
|
) * 100
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def high_failure_rate(self):
|
|
|
|
|
|
return self.failure_rate > 30
|
|
|
|
|
|
|
2020-01-08 12:23:09 +00:00
|
|
|
|
def get_notifications(self, status):
|
|
|
|
|
|
return notification_api_client.get_notifications_for_service(
|
|
|
|
|
|
self.service, self.id, status=status,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def cancel(self):
|
|
|
|
|
|
if self.template_type == 'letter':
|
|
|
|
|
|
return job_api_client.cancel_letter_job(self.service, self.id)
|
|
|
|
|
|
else:
|
|
|
|
|
|
return job_api_client.cancel_job(self.service, self.id)
|
2020-01-08 14:29:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ImmediateJobs(ModelList):
|
2020-01-16 15:57:51 +00:00
|
|
|
|
client_method = job_api_client.get_immediate_jobs
|
2020-01-08 14:29:56 +00:00
|
|
|
|
model = Job
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ScheduledJobs(ImmediateJobs):
|
2020-01-16 15:57:51 +00:00
|
|
|
|
client_method = job_api_client.get_scheduled_jobs
|
2020-01-08 14:29:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PaginatedJobs(ImmediateJobs):
|
|
|
|
|
|
|
2020-01-16 15:57:51 +00:00
|
|
|
|
client_method = job_api_client.get_page_of_jobs
|
2020-01-08 14:29:56 +00:00
|
|
|
|
|
2020-01-08 16:32:08 +00:00
|
|
|
|
def __init__(self, service_id, page=None):
|
2020-01-08 14:29:56 +00:00
|
|
|
|
try:
|
|
|
|
|
|
self.current_page = int(page)
|
|
|
|
|
|
except TypeError:
|
|
|
|
|
|
self.current_page = 1
|
2020-01-16 15:57:51 +00:00
|
|
|
|
response = self.client_method(service_id, page=self.current_page)
|
2020-01-08 14:29:56 +00:00
|
|
|
|
self.items = response['data']
|
2020-01-08 16:32:08 +00:00
|
|
|
|
self.prev_page = response.get('links', {}).get('prev', None)
|
|
|
|
|
|
self.next_page = response.get('links', {}).get('next', None)
|
2020-01-08 14:29:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PaginatedUploads(PaginatedJobs):
|
2020-01-16 15:57:51 +00:00
|
|
|
|
client_method = job_api_client.get_uploads
|