mirror of
https://github.com/GSA/notifications-admin.git
synced 2025-12-14 17:13:25 -05:00
Make job page poll for updates
This is a first go at having the job page update without refreshing. The approach I’ve taken is to do all the rendering of HTML on the server side, rather than use a Javascipt templating engine like mustache. This ensures that we don’t have to maintain two sets of templates. So the approach is to split the job page into partials. These partials can then: - be included in the job page to render the whole page - be rendered indivudually and then returned as a blob of HTML inside a JSON response Then I’ve added a Javascript module which looks for areas of the page that should be reloaded. For each area of the page it will poll a URL and re-render that section of the page when it gets new HTML. It implements some throttling so that API calls will never happen more frequently than 0.67 times/second.
This commit is contained in:
44
app/assets/javascripts/updateContent.js
Normal file
44
app/assets/javascripts/updateContent.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
(function(GOVUK, Modules) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const interval = 1500; // milliseconds
|
||||||
|
|
||||||
|
GOVUK.timeCache = {};
|
||||||
|
GOVUK.resultCache = {};
|
||||||
|
|
||||||
|
let getter = function(resource, render) {
|
||||||
|
|
||||||
|
if (
|
||||||
|
GOVUK.resultCache[resource] &&
|
||||||
|
(Date.now() < GOVUK.timeCache[resource])
|
||||||
|
) {
|
||||||
|
render(GOVUK.resultCache[resource]);
|
||||||
|
} else {
|
||||||
|
GOVUK.timeCache[resource] = Date.now() + interval;
|
||||||
|
$.get(
|
||||||
|
resource,
|
||||||
|
response => render(GOVUK.resultCache[resource] = response)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
let poller = (resource, key, component) => () => getter(
|
||||||
|
resource, response => component.html(response[key])
|
||||||
|
);
|
||||||
|
|
||||||
|
Modules.UpdateContent = function() {
|
||||||
|
|
||||||
|
this.start = function(component) {
|
||||||
|
|
||||||
|
const $component = $(component);
|
||||||
|
|
||||||
|
setInterval(
|
||||||
|
poller($component.data('resource'), $component.data('key'), $component),
|
||||||
|
interval / 5
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
})(window.GOVUK, window.GOVUK.Modules);
|
||||||
@@ -4,7 +4,8 @@ import time
|
|||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
render_template,
|
render_template,
|
||||||
abort
|
abort,
|
||||||
|
jsonify
|
||||||
)
|
)
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from notifications_python_client.errors import HTTPError
|
from notifications_python_client.errors import HTTPError
|
||||||
@@ -15,8 +16,6 @@ from app.main import main
|
|||||||
from app.main.dao import templates_dao
|
from app.main.dao import templates_dao
|
||||||
from app.main.dao import services_dao
|
from app.main.dao import services_dao
|
||||||
|
|
||||||
now = time.strftime('%H:%M')
|
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<service_id>/jobs")
|
@main.route("/services/<service_id>/jobs")
|
||||||
@login_required
|
@login_required
|
||||||
@@ -24,7 +23,7 @@ def view_jobs(service_id):
|
|||||||
try:
|
try:
|
||||||
jobs = job_api_client.get_job(service_id)['data']
|
jobs = job_api_client.get_job(service_id)['data']
|
||||||
return render_template(
|
return render_template(
|
||||||
'views/jobs.html',
|
'views/jobs/jobs.html',
|
||||||
jobs=jobs,
|
jobs=jobs,
|
||||||
service_id=service_id
|
service_id=service_id
|
||||||
)
|
)
|
||||||
@@ -44,16 +43,16 @@ def view_job(service_id, job_id):
|
|||||||
notifications = notification_api_client.get_notifications_for_service(service_id, job_id)
|
notifications = notification_api_client.get_notifications_for_service(service_id, job_id)
|
||||||
finished = job['status'] == 'finished'
|
finished = job['status'] == 'finished'
|
||||||
return render_template(
|
return render_template(
|
||||||
'views/job.html',
|
'views/jobs/job.html',
|
||||||
notifications=notifications['notifications'],
|
notifications=notifications['notifications'],
|
||||||
counts={
|
counts={
|
||||||
'queued': 0 if finished else job['notification_count'],
|
'queued': 0 if finished else job['notification_count'],
|
||||||
'sent': job['notification_count'] if finished else 0,
|
'sent': job['notification_count'] if finished else 0,
|
||||||
'failed': 0
|
'failed': 0,
|
||||||
|
'cost': u'£0.00'
|
||||||
},
|
},
|
||||||
uploaded_at=job['created_at'],
|
uploaded_at=job['created_at'],
|
||||||
finished_at=job['updated_at'] if finished else None,
|
finished_at=job['updated_at'] if finished else None,
|
||||||
cost=u'£0.00',
|
|
||||||
uploaded_file_name=job['original_file_name'],
|
uploaded_file_name=job['original_file_name'],
|
||||||
template=Template(
|
template=Template(
|
||||||
templates_dao.get_service_template_or_404(service_id, job['template'])['data'],
|
templates_dao.get_service_template_or_404(service_id, job['template'])['data'],
|
||||||
@@ -70,9 +69,47 @@ def view_job(service_id, job_id):
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/services/<service_id>/jobs/<job_id>.json")
|
||||||
|
@login_required
|
||||||
|
def view_job_updates(service_id, job_id):
|
||||||
|
service = services_dao.get_service_by_id_or_404(service_id)
|
||||||
|
try:
|
||||||
|
job = job_api_client.get_job(service_id, job_id)['data']
|
||||||
|
notifications = notification_api_client.get_notifications_for_service(service_id, job_id)
|
||||||
|
finished = job['status'] == 'finished'
|
||||||
|
return jsonify(**{
|
||||||
|
'counts': render_template(
|
||||||
|
'partials/jobs/count.html',
|
||||||
|
counts={
|
||||||
|
'queued': 0 if finished else job['notification_count'],
|
||||||
|
'sent': job['notification_count'] if finished else 0,
|
||||||
|
'failed': 0,
|
||||||
|
'cost': u'£0.00'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
'notifications': render_template(
|
||||||
|
'partials/jobs/notifications.html',
|
||||||
|
notifications=notifications['notifications']
|
||||||
|
),
|
||||||
|
'status': render_template(
|
||||||
|
'partials/jobs/status.html',
|
||||||
|
uploaded_at=job['created_at'],
|
||||||
|
finished_at=job['updated_at'] if finished else None
|
||||||
|
),
|
||||||
|
})
|
||||||
|
except HTTPError as e:
|
||||||
|
if e.status_code == 404:
|
||||||
|
abort(404)
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<service_id>/jobs/<job_id>/notification/<string:notification_id>")
|
@main.route("/services/<service_id>/jobs/<job_id>/notification/<string:notification_id>")
|
||||||
@login_required
|
@login_required
|
||||||
def view_notification(service_id, job_id, notification_id):
|
def view_notification(service_id, job_id, notification_id):
|
||||||
|
|
||||||
|
now = time.strftime('%H:%M')
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'views/notification.html',
|
'views/notification.html',
|
||||||
message=[
|
message=[
|
||||||
|
|||||||
24
app/templates/partials/jobs/count.html
Normal file
24
app/templates/partials/jobs/count.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% from "components/big-number.html" import big_number %}
|
||||||
|
<ul class="grid-row job-totals">
|
||||||
|
<li class="column-one-quarter">
|
||||||
|
{{ big_number(
|
||||||
|
counts.queued, 'queued'
|
||||||
|
)}}
|
||||||
|
</li>
|
||||||
|
<li class="column-one-quarter">
|
||||||
|
{{ big_number(
|
||||||
|
counts.sent, 'sent'
|
||||||
|
)}}
|
||||||
|
</li>
|
||||||
|
<li class="column-one-quarter">
|
||||||
|
{{ big_number(
|
||||||
|
counts.failed,
|
||||||
|
'failed'
|
||||||
|
)}}
|
||||||
|
</li>
|
||||||
|
<li class="column-one-quarter">
|
||||||
|
{{ big_number(
|
||||||
|
counts.cost, 'total cost'
|
||||||
|
)}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
22
app/templates/partials/jobs/notifications.html
Normal file
22
app/templates/partials/jobs/notifications.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{% from "components/table.html" import list_table, field, right_aligned_field_heading %}
|
||||||
|
|
||||||
|
{% call(item) list_table(
|
||||||
|
notifications,
|
||||||
|
caption=uploaded_file_name,
|
||||||
|
caption_visible=False,
|
||||||
|
empty_message="No messages to show yet",
|
||||||
|
field_headings=[
|
||||||
|
'Recipient',
|
||||||
|
right_aligned_field_heading('Status')
|
||||||
|
]
|
||||||
|
) %}
|
||||||
|
{% call field() %}
|
||||||
|
{{ item.to }}
|
||||||
|
{% endcall %}
|
||||||
|
{% call field(
|
||||||
|
align='right',
|
||||||
|
status='error' if item.status == 'Failed' else 'default'
|
||||||
|
) %}
|
||||||
|
{{ item.status|title }} at {{ item.sent_at|format_time }}
|
||||||
|
{% endcall %}
|
||||||
|
{% endcall %}
|
||||||
9
app/templates/partials/jobs/status.html
Normal file
9
app/templates/partials/jobs/status.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{% if finished_at %}
|
||||||
|
<p class='heading-small'>
|
||||||
|
Finished {{ finished_at|format_datetime }}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p class='heading-small'>
|
||||||
|
Started {{ uploaded_at|format_datetime }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
{% extends "withnav_template.html" %}
|
|
||||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading %}
|
|
||||||
{% from "components/big-number.html" import big_number %}
|
|
||||||
{% from "components/banner.html" import banner %}
|
|
||||||
{% from "components/sms-message.html" import sms_message %}
|
|
||||||
{% from "components/email-message.html" import email_message %}
|
|
||||||
|
|
||||||
{% block page_title %}
|
|
||||||
{{ uploaded_file_name }} – GOV.UK Notify
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
|
||||||
|
|
||||||
<h1 class="heading-large">
|
|
||||||
{{ uploaded_file_name }}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{% if 'sms' == template.template_type %}
|
|
||||||
<div class="grid-row">
|
|
||||||
<div class="column-two-thirds">
|
|
||||||
{{ sms_message(
|
|
||||||
template.formatted_as_markup,
|
|
||||||
)}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% elif 'email' == template.template_type %}
|
|
||||||
{{ email_message(
|
|
||||||
template.subject,
|
|
||||||
template,
|
|
||||||
from_address='{}@notifications.service.gov.uk'.format(service.email_from),
|
|
||||||
from_name=from_name
|
|
||||||
)}}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if finished_at %}
|
|
||||||
<p class='heading-small'>
|
|
||||||
Finished {{ finished_at|format_datetime }}
|
|
||||||
</p>
|
|
||||||
{% else %}
|
|
||||||
<p class='heading-small'>
|
|
||||||
Started {{ uploaded_at|format_datetime }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<ul class="grid-row job-totals">
|
|
||||||
<li class="column-one-quarter">
|
|
||||||
{{ big_number(
|
|
||||||
counts.queued, 'queued'
|
|
||||||
)}}
|
|
||||||
</li>
|
|
||||||
<li class="column-one-quarter">
|
|
||||||
{{ big_number(
|
|
||||||
counts.sent, 'sent'
|
|
||||||
)}}
|
|
||||||
</li>
|
|
||||||
<li class="column-one-quarter">
|
|
||||||
{{ big_number(
|
|
||||||
counts.failed,
|
|
||||||
'failed'
|
|
||||||
)}}
|
|
||||||
</li>
|
|
||||||
<li class="column-one-quarter">
|
|
||||||
{{ big_number(
|
|
||||||
cost, 'total cost'
|
|
||||||
)}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
|
|
||||||
{% if notifications %}
|
|
||||||
{% call(item) list_table(
|
|
||||||
notifications,
|
|
||||||
caption=uploaded_file_name,
|
|
||||||
caption_visible=False,
|
|
||||||
empty_message="Messages go here",
|
|
||||||
field_headings=[
|
|
||||||
'Recipient',
|
|
||||||
right_aligned_field_heading('Status')
|
|
||||||
]
|
|
||||||
) %}
|
|
||||||
{% call field() %}
|
|
||||||
{{ item.to }}
|
|
||||||
{% endcall %}
|
|
||||||
{% call field(
|
|
||||||
align='right',
|
|
||||||
status='error' if item.status == 'Failed' else 'default'
|
|
||||||
) %}
|
|
||||||
{{ item.status|title }} at {{ item.sent_at|format_time }}
|
|
||||||
{% endcall %}
|
|
||||||
{% endcall %}
|
|
||||||
{% else %}
|
|
||||||
<p>
|
|
||||||
<a href="{{ url_for(".view_job", service_id=service_id, job_id=job_id) }}">Refresh</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
69
app/templates/views/jobs/job.html
Normal file
69
app/templates/views/jobs/job.html
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{% extends "withnav_template.html" %}
|
||||||
|
{% from "components/banner.html" import banner %}
|
||||||
|
{% from "components/sms-message.html" import sms_message %}
|
||||||
|
{% from "components/email-message.html" import email_message %}
|
||||||
|
|
||||||
|
{% block page_title %}
|
||||||
|
{{ uploaded_file_name }} – GOV.UK Notify
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
|
<h1 class="heading-large">
|
||||||
|
{{ uploaded_file_name }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{% if 'sms' == template.template_type %}
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="column-two-thirds">
|
||||||
|
{{ sms_message(
|
||||||
|
template.formatted_as_markup,
|
||||||
|
)}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif 'email' == template.template_type %}
|
||||||
|
{{ email_message(
|
||||||
|
template.subject,
|
||||||
|
template,
|
||||||
|
from_address='{}@notifications.service.gov.uk'.format(service.email_from),
|
||||||
|
from_name=from_name
|
||||||
|
)}}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not finished_at %}
|
||||||
|
<div
|
||||||
|
data-module="update-content"
|
||||||
|
data-resource="{{url_for(".view_job_updates", service_id=service_id, job_id=job_id)}}"
|
||||||
|
data-key="status"
|
||||||
|
>
|
||||||
|
{% endif %}
|
||||||
|
{% include 'partials/jobs/status.html' %}
|
||||||
|
{% if not finished_at %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not finished_at %}
|
||||||
|
<div
|
||||||
|
data-module="update-content"
|
||||||
|
data-resource="{{url_for(".view_job_updates", service_id=service_id, job_id=job_id)}}"
|
||||||
|
data-key="counts"
|
||||||
|
>
|
||||||
|
{% endif %}
|
||||||
|
{% include 'partials/jobs/count.html' %}
|
||||||
|
{% if not finished_at %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not finished_at %}
|
||||||
|
<div
|
||||||
|
data-module="update-content"
|
||||||
|
data-resource="{{url_for(".view_job_updates", service_id=service_id, job_id=job_id)}}"
|
||||||
|
data-key="notifications"
|
||||||
|
>
|
||||||
|
{% endif %}
|
||||||
|
{% include 'partials/jobs/notifications.html' %}
|
||||||
|
{% if not finished_at %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -54,6 +54,7 @@ gulp.task('javascripts', () => gulp
|
|||||||
paths.src + 'javascripts/autofocus.js',
|
paths.src + 'javascripts/autofocus.js',
|
||||||
paths.src + 'javascripts/highlightTags.js',
|
paths.src + 'javascripts/highlightTags.js',
|
||||||
paths.src + 'javascripts/fileUpload.js',
|
paths.src + 'javascripts/fileUpload.js',
|
||||||
|
paths.src + 'javascripts/updateContent.js',
|
||||||
paths.src + 'javascripts/main.js'
|
paths.src + 'javascripts/main.js'
|
||||||
])
|
])
|
||||||
.pipe(plugins.babel({
|
.pipe(plugins.babel({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from flask import url_for
|
from flask import url_for
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
def test_should_return_list_of_all_jobs(app_,
|
def test_should_return_list_of_all_jobs(app_,
|
||||||
@@ -21,17 +22,19 @@ def test_should_return_list_of_all_jobs(app_,
|
|||||||
assert len(jobs) == 5
|
assert len(jobs) == 5
|
||||||
|
|
||||||
|
|
||||||
def test_should_show_page_for_one_job(app_,
|
def test_should_show_page_for_one_job(
|
||||||
service_one,
|
app_,
|
||||||
api_user_active,
|
service_one,
|
||||||
mock_login,
|
api_user_active,
|
||||||
mock_get_user,
|
mock_login,
|
||||||
mock_get_user_by_email,
|
mock_get_user,
|
||||||
mock_get_service,
|
mock_get_user_by_email,
|
||||||
mock_get_service_template,
|
mock_get_service,
|
||||||
job_data,
|
mock_get_service_template,
|
||||||
mock_get_job,
|
job_data,
|
||||||
mock_get_notifications):
|
mock_get_job,
|
||||||
|
mock_get_notifications
|
||||||
|
):
|
||||||
service_id = job_data['service']
|
service_id = job_data['service']
|
||||||
job_id = job_data['id']
|
job_id = job_data['id']
|
||||||
file_name = job_data['original_file_name']
|
file_name = job_data['original_file_name']
|
||||||
@@ -45,3 +48,35 @@ def test_should_show_page_for_one_job(app_,
|
|||||||
content = response.get_data(as_text=True)
|
content = response.get_data(as_text=True)
|
||||||
assert "Test Service: Your vehicle tax is about to expire" in content
|
assert "Test Service: Your vehicle tax is about to expire" in content
|
||||||
assert file_name in content
|
assert file_name in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_show_updates_for_one_job_as_json(
|
||||||
|
app_,
|
||||||
|
service_one,
|
||||||
|
api_user_active,
|
||||||
|
mock_login,
|
||||||
|
mock_get_user,
|
||||||
|
mock_get_user_by_email,
|
||||||
|
mock_get_service,
|
||||||
|
mock_get_service_template,
|
||||||
|
job_data,
|
||||||
|
mock_get_job,
|
||||||
|
mock_get_notifications
|
||||||
|
):
|
||||||
|
service_id = job_data['service']
|
||||||
|
job_id = job_data['id']
|
||||||
|
file_name = job_data['original_file_name']
|
||||||
|
|
||||||
|
with app_.test_request_context():
|
||||||
|
with app_.test_client() as client:
|
||||||
|
client.login(api_user_active)
|
||||||
|
response = client.get(url_for('main.view_job_updates', service_id=service_id, job_id=job_id))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = json.loads(response.get_data(as_text=True))
|
||||||
|
assert 'sent' in content['counts']
|
||||||
|
assert 'queued' in content['counts']
|
||||||
|
assert 'failed' in content['counts']
|
||||||
|
assert 'Recipient' in content['notifications']
|
||||||
|
assert 'Status' in content['notifications']
|
||||||
|
assert 'Started' in content['status']
|
||||||
|
|||||||
Reference in New Issue
Block a user