Group uploaded letters by day of printing

Some teams have started uploading quite a lot of letters (in the
hundreds per week). They’re also uploading CSVs of emails. This means
the uploads page ends up quite jumbled.

This is because:
- there’s just a lot of items to scan through
- conceptually it’s a bit odd to have batches of things displayed
  alongside individual things on the same page

So instead this commit starts grouping together uploaded letters. It
does this by the date on which we ‘start’ printing them, or in other
words the time at which they can no longer be cancelled.

This feels like a natural grouping, and it matches what we know about
people’s mental models of ‘batches’ and ‘runs’ when talking about
printing.

The code for this is a bit gnarly because:
- timezones
- the print cutoff doesn’t align with the end of a day
- we have to do this in SQL because it wouldn’t be efficient to query
  thousands of letters and then do the timezone calculations on them in
  Python
This commit is contained in:
Chris Hill-Scott
2020-05-11 10:51:33 +01:00
parent 79646f7b4a
commit 421c1aac96
4 changed files with 179 additions and 99 deletions

View File

@@ -1,4 +1,5 @@
from datetime import datetime, timedelta
from freezegun import freeze_time
from app.dao.uploads_dao import dao_get_uploads_by_service_id
from app.models import LETTER_TYPE, JOB_STATUS_IN_PROGRESS
@@ -30,6 +31,7 @@ def create_uploaded_template(service):
)
@freeze_time("2020-02-02 14:00") # GMT time
def test_get_uploads_for_service(sample_template):
create_service_data_retention(sample_template.service, 'sms', days_of_retention=9)
job = create_job(sample_template, processing_started=datetime.utcnow())
@@ -40,7 +42,7 @@ def test_get_uploads_for_service(sample_template):
other_template = create_template(service=other_service)
other_job = create_job(other_template, processing_started=datetime.utcnow())
other_letter_template = create_uploaded_template(other_service)
other_letter = create_uploaded_letter(other_letter_template, other_service)
create_uploaded_letter(other_letter_template, other_service)
uploads_from_db = dao_get_uploads_by_service_id(job.service_id).items
other_uploads_from_db = dao_get_uploads_by_service_id(other_job.service_id).items
@@ -48,17 +50,17 @@ def test_get_uploads_for_service(sample_template):
assert len(uploads_from_db) == 2
assert uploads_from_db[0] == (
letter.id,
letter.client_reference,
None,
'Uploaded letters',
1,
'letter',
None,
7,
letter.created_at,
letter.created_at.replace(hour=17, minute=30, second=0, microsecond=0),
None,
letter.created_at.replace(hour=17, minute=30, second=0, microsecond=0),
None,
'letter_day',
None,
letter.created_at,
letter.status,
"letter",
"file-name",
)
assert uploads_from_db[1] == (
job.id,
@@ -75,17 +77,19 @@ def test_get_uploads_for_service(sample_template):
)
assert len(other_uploads_from_db) == 2
assert other_uploads_from_db[0] == (other_letter.id,
other_letter.client_reference,
1,
None,
7,
other_letter.created_at,
None,
other_letter.created_at,
other_letter.status,
"letter",
"file-name")
assert other_uploads_from_db[0] == (
None,
'Uploaded letters',
1,
'letter',
None,
letter.created_at.replace(hour=17, minute=30, second=0, microsecond=0),
None,
letter.created_at.replace(hour=17, minute=30, second=0, microsecond=0),
None,
"letter_day",
None,
)
assert other_uploads_from_db[1] == (other_job.id,
other_job.original_file_name,
other_job.notification_count,
@@ -98,10 +102,48 @@ def test_get_uploads_for_service(sample_template):
"job",
None)
assert uploads_from_db[0] != other_uploads_from_db[0]
assert uploads_from_db[1] != other_uploads_from_db[1]
@freeze_time("2020-02-02 18:00")
def test_get_uploads_for_service_groups_letters(sample_template):
letter_template = create_uploaded_template(sample_template.service)
# Just gets into yesterdays print run
create_uploaded_letter(letter_template, sample_template.service, created_at=(
datetime(2020, 2, 1, 17, 29, 59)
))
# Yesterday but in todays print run
create_uploaded_letter(letter_template, sample_template.service, created_at=(
datetime(2020, 2, 1, 17, 30)
))
# First thing today
create_uploaded_letter(letter_template, sample_template.service, created_at=(
datetime(2020, 2, 2, 0, 0)
))
# Just before todays print deadline
create_uploaded_letter(letter_template, sample_template.service, created_at=(
datetime(2020, 2, 2, 17, 29, 59)
))
# Just missed todays print deadline
create_uploaded_letter(letter_template, sample_template.service, created_at=(
datetime(2020, 2, 2, 17, 30)
))
uploads_from_db = dao_get_uploads_by_service_id(sample_template.service_id).items
assert [
(upload.notification_count, upload.created_at)
for upload in uploads_from_db
] == [
(1, datetime(2020, 2, 3, 17, 30)),
(3, datetime(2020, 2, 2, 17, 30)),
(1, datetime(2020, 2, 1, 17, 30)),
]
def test_get_uploads_does_not_return_cancelled_jobs_or_letters(sample_template):
create_job(sample_template, job_status='scheduled')
create_job(sample_template, job_status='cancelled')
@@ -118,14 +160,17 @@ def test_get_uploads_orders_by_created_at_desc(sample_template):
job_status=JOB_STATUS_IN_PROGRESS)
upload_2 = create_job(sample_template, processing_started=datetime.utcnow(),
job_status=JOB_STATUS_IN_PROGRESS)
upload_3 = create_uploaded_letter(letter_template, sample_template.service, status='delivered')
create_uploaded_letter(letter_template, sample_template.service, status='delivered')
results = dao_get_uploads_by_service_id(service_id=sample_template.service_id).items
assert len(results) == 3
assert results[0].id == upload_3.id
assert results[1].id == upload_2.id
assert results[2].id == upload_1.id
assert [
(result.id, result.upload_type) for result in results
] == [
(None, 'letter_day'),
(upload_2.id, 'job'),
(upload_1.id, 'job'),
]
def test_get_uploads_orders_by_processing_started_desc(sample_template):
@@ -148,25 +193,26 @@ def test_get_uploads_orders_by_processing_started_and_created_at_desc(sample_tem
letter_template = create_uploaded_template(sample_template.service)
days_ago = datetime.utcnow() - timedelta(days=4)
upload_1 = create_uploaded_letter(letter_template, service=letter_template.service)
create_uploaded_letter(letter_template, service=letter_template.service)
upload_2 = create_job(sample_template, processing_started=datetime.utcnow() - timedelta(days=1),
created_at=days_ago,
job_status=JOB_STATUS_IN_PROGRESS)
upload_3 = create_job(sample_template, processing_started=datetime.utcnow() - timedelta(days=2),
created_at=days_ago,
job_status=JOB_STATUS_IN_PROGRESS)
upload_4 = create_uploaded_letter(letter_template, service=letter_template.service,
created_at=datetime.utcnow() - timedelta(days=3))
create_uploaded_letter(letter_template, service=letter_template.service,
created_at=datetime.utcnow() - timedelta(days=3))
results = dao_get_uploads_by_service_id(service_id=sample_template.service_id).items
assert len(results) == 4
assert results[0].id == upload_1.id
assert results[0].id is None
assert results[1].id == upload_2.id
assert results[2].id == upload_3.id
assert results[3].id == upload_4.id
assert results[3].id is None
@freeze_time('2020-04-02 14:00') # Few days after the clocks go forward
def test_get_uploads_only_gets_uploads_within_service_retention_period(sample_template):
letter_template = create_uploaded_template(sample_template.service)
create_service_data_retention(sample_template.service, 'sms', days_of_retention=3)
@@ -199,36 +245,58 @@ def test_get_uploads_only_gets_uploads_within_service_retention_period(sample_te
results = dao_get_uploads_by_service_id(service_id=sample_template.service_id).items
assert len(results) == 4
assert results[0].id == upload_1.id
assert results[1].id == upload_2.id
assert results[2].id == upload_3.id
assert results[3].id == upload_4.id
# Uploaded letters get their `created_at` shifted time of printing
# 17:30 BST == 16:30 UTC
assert results[0].created_at == upload_1.created_at.replace(hour=16, minute=30, second=0, microsecond=0)
# Jobs keep their original `created_at`
assert results[1].created_at == upload_2.created_at.replace(hour=14, minute=00, second=0, microsecond=0)
# Still in BST here…
assert results[2].created_at == upload_3.created_at.replace(hour=16, minute=30, second=0, microsecond=0)
# Now weve gone far enough back to be in GMT
# 17:30 GMT == 17:30 UTC
assert results[3].created_at == upload_4.created_at.replace(hour=17, minute=30, second=0, microsecond=0)
@freeze_time('2020-02-02 14:00')
def test_get_uploads_is_paginated(sample_template):
letter_template = create_uploaded_template(sample_template.service)
upload_1 = create_uploaded_letter(letter_template, sample_template.service, status='delivered',
created_at=datetime.utcnow() - timedelta(minutes=3))
upload_2 = create_job(sample_template, processing_started=datetime.utcnow() - timedelta(minutes=2),
job_status=JOB_STATUS_IN_PROGRESS)
upload_3 = create_uploaded_letter(letter_template, sample_template.service, status='delivered',
created_at=datetime.utcnow() - timedelta(minutes=1))
upload_4 = create_job(sample_template, processing_started=datetime.utcnow(), job_status=JOB_STATUS_IN_PROGRESS)
create_uploaded_letter(
letter_template, sample_template.service, status='delivered',
created_at=datetime.utcnow() - timedelta(minutes=3),
)
create_job(
sample_template, processing_started=datetime.utcnow() - timedelta(minutes=2),
job_status=JOB_STATUS_IN_PROGRESS,
)
create_uploaded_letter(
letter_template, sample_template.service, status='delivered',
created_at=datetime.utcnow() - timedelta(minutes=1),
)
create_job(
sample_template, processing_started=datetime.utcnow(),
job_status=JOB_STATUS_IN_PROGRESS,
)
results = dao_get_uploads_by_service_id(sample_template.service_id, page=1, page_size=2)
results = dao_get_uploads_by_service_id(sample_template.service_id, page=1, page_size=1)
assert results.per_page == 2
assert results.total == 4
assert len(results.items) == 2
assert results.items[0].id == upload_4.id
assert results.items[1].id == upload_3.id
assert results.per_page == 1
assert results.total == 3
assert len(results.items) == 1
assert results.items[0].created_at == datetime.utcnow().replace(hour=17, minute=30, second=0, microsecond=0)
assert results.items[0].notification_count == 2
assert results.items[0].upload_type == 'letter_day'
results = dao_get_uploads_by_service_id(sample_template.service_id, page=2, page_size=2)
results = dao_get_uploads_by_service_id(sample_template.service_id, page=2, page_size=1)
assert len(results.items) == 2
assert results.items[0].id == upload_2.id
assert results.items[1].id == upload_1.id
assert len(results.items) == 1
assert results.items[0].created_at == datetime.utcnow().replace(hour=14, minute=0, second=0, microsecond=0)
assert results.items[0].notification_count == 1
assert results.items[0].upload_type == 'job'
def test_get_uploads_returns_empty_list(sample_service):