Show jobs on contact list

It’s a bit unintuitive that starting a job from a contact list makes a
copy of the file, which has no relationship to the list it was copied
from. This is more of an implementation detail, rather than something
that comes from people’s mental models of what is going on. Or at least
that’s what I hypothesise.

I think it’s clearer to show jobs that come from contact lists within
the lists that they were created from. By naming the jobs by template
this gives a clearer view of what messages have been sent to the group
over time.
This commit is contained in:
Chris Hill-Scott
2020-05-12 17:31:01 +01:00
parent 149456b73a
commit 423875011c
9 changed files with 257 additions and 61 deletions

View File

@@ -513,9 +513,11 @@ def save_contact_list(service_id, upload_id):
@main.route("/services/<uuid:service_id>/contact-list/<uuid:contact_list_id>", methods=['GET'])
@user_has_permissions('send_messages')
def contact_list(service_id, contact_list_id):
contact_list = ContactList.from_id(contact_list_id, service_id=service_id)
return render_template(
'views/uploads/contact-list/contact-list.html',
contact_list=ContactList.from_id(contact_list_id, service_id=service_id),
contact_list=contact_list,
jobs=contact_list.get_jobs(page=1),
)

View File

@@ -8,7 +8,7 @@ from notifications_utils.timezones import utc_string_to_aware_gmt_datetime
from werkzeug.utils import cached_property
from app.models import JSONModel, ModelList
from app.models.job import PaginatedJobs
from app.models.job import PaginatedJobsAndScheduledJobs
from app.notify_client.contact_list_api_client import contact_list_api_client
from app.s3_client.s3_csv_client import (
get_csv_metadata,
@@ -136,7 +136,7 @@ class ContactList(JSONModel):
return f'{file_name}.csv'
def get_jobs(self, *, page):
return PaginatedJobs(
return PaginatedJobsAndScheduledJobs(
self.service_id,
contact_list_id=self.id,
page=page,

View File

@@ -233,9 +233,19 @@ class ScheduledJobs(ImmediateJobs):
class PaginatedJobs(PaginatedModelList, ImmediateJobs):
client_method = job_api_client.get_page_of_jobs
statuses = None
def __init__(self, service_id, *, contact_list_id=None, page=None):
super().__init__(service_id, contact_list_id=contact_list_id, page=page)
super().__init__(
service_id,
contact_list_id=contact_list_id,
statuses=self.statuses,
page=page,
)
class PaginatedJobsAndScheduledJobs(PaginatedJobs):
statuses = job_api_client.NON_CANCELLED_JOB_STATUSES
class PaginatedUploads(PaginatedModelList, ImmediateJobs):

View File

@@ -16,6 +16,7 @@ class JobApiClient(NotifyAdminAPIClient):
}
SCHEDULED_JOB_STATUS = 'scheduled'
CANCELLED_JOB_STATUS = 'cancelled'
NON_CANCELLED_JOB_STATUSES = JOB_STATUSES - {CANCELLED_JOB_STATUS}
NON_SCHEDULED_JOB_STATUSES = JOB_STATUSES - {SCHEDULED_JOB_STATUS, CANCELLED_JOB_STATUS}
def get_job(self, service_id, job_id):
@@ -52,10 +53,10 @@ class JobApiClient(NotifyAdminAPIClient):
if job['job_status'] != 'cancelled'
)
def get_page_of_jobs(self, service_id, *, page, contact_list_id=None):
def get_page_of_jobs(self, service_id, *, page, statuses=None, contact_list_id=None):
return self.get_jobs(
service_id,
statuses=self.NON_SCHEDULED_JOB_STATUSES,
statuses=statuses or self.NON_SCHEDULED_JOB_STATUSES,
page=page,
contact_list_id=contact_list_id,
)

View File

@@ -1,9 +1,10 @@
{% extends "withnav_template.html" %}
{% from "components/banner.html" import banner_wrapper %}
{% from "components/big-number.html" import big_number %}
{% from "components/radios.html" import radio_select %}
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %}
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading, row, row_heading %}
{% from "components/page-header.html" import page_header %}
{% from "components/message-count-label.html" import message_count_label, recipient_count_label %}
{% from "components/message-count-label.html" import message_count_label, recipient_count, recipient_count_label, iteration_count %}
{% from "components/button/macro.njk" import govukButton %}
{% block service_page_title %}
@@ -18,41 +19,124 @@
) }}
<p class="govuk-body">
Uploaded by {{ contact_list.created_by }} {{ contact_list.created_at|format_datetime_human }}
Uploaded by {{ contact_list.created_by }} {{ contact_list.created_at|format_datetime_human }}.
</p>
<p class="govuk-body">
<a class="govuk-link govuk-link--no-visited-state heading-small" download href="{{ url_for('main.download_contact_list', service_id=current_service.id, contact_list_id=contact_list.id) }}">Download this list</a>&emsp;
{{ contact_list.recipients|length|format_thousands }}
{{ recipient_count_label(contact_list.recipients|length, contact_list.recipients.template_type) }}
</p>
{% if jobs %}
<p class="govuk-body">
Used {{ iteration_count(jobs|length) }}
in the last {{ current_service.get_days_of_retention(contact_list.template_type) }}
days.
</p>
<div class='dashboard-table ajax-block-container'>
{% call(item, row_number) list_table(
jobs,
caption="Messages sent from this contact list",
caption_visible=False,
empty_message='',
field_headings=[
'Template',
'Status'
],
field_headings_visible=False
) %}
{% call row_heading() %}
<div class="file-list">
<a class="file-list-filename-large govuk-link govuk-link--no-visited-state" href="{{ url_for('.view_job', service_id=current_service.id, job_id=item.id) }}">{{ item.template_name }}</a>
{% if item.scheduled %}
<span class="file-list-hint-large">
Sending {{
item.scheduled_for|format_datetime_relative
}}
</span>
{% else %}
<span class="file-list-hint-large">
Sent {{
(item.scheduled_for or item.created_at)|format_datetime_relative
}}
</span>
{% endif %}
{% set recipient_column = contact_list.recipients.column_headers[0] %}
{% call(item, row_number) list_table(
contact_list.recipients.displayed_rows,
caption=recipient_count_label(contact_list.recipients|length, contact_list.template_type)|capitalize,
caption_visible=False,
field_headings=['1', recipient_column],
) %}
{{ index_field(row_number) }}
{{ text_field(item[recipient_column].data) }}
{% endcall %}
{% if contact_list.recipients.displayed_rows|list|length < contact_list.recipients|length %}
<p class="table-show-more-link">
Only showing the first {{ contact_list.recipients.displayed_rows|list|length }} rows
</div>
{% endcall %}
{% call field() %}
{% if item.scheduled %}
{{ big_number(
item.notification_count,
smallest=True,
label=message_count_label(
item.notification_count,
item.template_type,
suffix='waiting to send'
)
) }}
{% else %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-one-third">
{{ big_number(
item.notifications_sending,
smallest=True,
label='sending',
) }}
</div>
<div class="govuk-grid-column-one-third">
{{ big_number(item.notifications_delivered, smallest=True, label='delivered') }}
</div>
<div class="govuk-grid-column-one-third">
{{ big_number(item.notifications_failed, smallest=True, label='failed') }}
</div>
</div>
{% endif %}
{% endcall %}
{% endcall %}
</div>
{% else %}
<p class="govuk-body">
{% if contact_list.has_jobs %}
Not used in the last {{ current_service.get_days_of_retention(contact_list.template_type) }} days.
{% else %}
Not used yet.
{% endif %}
</p>
{% endif %}
{% if not confirm_delete_banner %}
<div class="js-stick-at-bottom-when-scrolling">
<div class="page-footer">
<h2 class="govuk-heading-m govuk-!-margin-bottom-2">
{{ recipient_count(contact_list.recipients|length, contact_list.template_type, prefix='saved') }}
</h2>
{% set recipient_column = contact_list.recipients.column_headers[0] %}
<div class="body-copy-table">
{% call(item, row_number) list_table(
contact_list.recipients.displayed_rows,
caption=recipient_count_label(contact_list.recipients|length, contact_list.template_type)|capitalize,
caption_visible=False,
field_headings=[recipient_column],
field_headings_visible=False,
) %}
{% if not loop.first %}
{{ text_field(item[recipient_column].data) }}
{% endif %}
{% endcall %}
{% if contact_list.recipients.displayed_rows|list|length < contact_list.recipients|length %}
<p class="table-show-more-link">
Only showing the first {{ contact_list.recipients.displayed_rows|list|length }} rows
</p>
{% endif %}
</div>
<div class="js-stick-at-bottom-when-scrolling">
<div class="page-footer">
{% if not confirm_delete_banner %}
<span class="page-footer-delete-link page-footer-delete-link-without-button">
<a class="govuk-link govuk-link--destructive" href="{{ url_for('main.delete_contact_list', service_id=current_service.id, contact_list_id=contact_list.id) }}">Delete this contact list</a>
</span>
</div>
{% endif %}
<a class="govuk-link govuk-link--no-visited-state page-footer-right-aligned-link-without-button" download href="{{ url_for('main.download_contact_list', service_id=current_service.id, contact_list_id=contact_list.id) }}">Download this contact list</a>
</div>
{% endif %}
</div>
{% endblock %}