Merge pull request #3514 from alphagov/broadcast-dashboard

Broadcast dashboard
This commit is contained in:
Chris Hill-Scott
2020-07-10 08:43:57 +01:00
committed by GitHub
8 changed files with 237 additions and 37 deletions

View File

@@ -59,7 +59,6 @@
padding-right: govuk-spacing(2);
background: $light-blue-25;
margin-right: govuk-spacing(3);
margin-bottom: 0;
}
}

View File

@@ -3,7 +3,7 @@ from flask import redirect, render_template, request, url_for
from app import current_service
from app.main import main
from app.main.forms import BroadcastAreaForm, SearchByNameForm
from app.models.broadcast_message import BroadcastMessage
from app.models.broadcast_message import BroadcastMessage, BroadcastMessages
from app.utils import service_has_permission, user_has_permissions
@@ -11,8 +11,11 @@ from app.utils import service_has_permission, user_has_permissions
@user_has_permissions()
@service_has_permission('broadcast')
def broadcast_dashboard(service_id):
broadcast_messages = BroadcastMessages(service_id)
return render_template(
'views/broadcast/dashboard.html',
live_broadcasts=broadcast_messages.with_status('broadcasting'),
previous_broadcasts=broadcast_messages.with_status('cancelled', 'completed'),
)

View File

@@ -4,7 +4,7 @@ from notifications_utils.broadcast_areas import broadcast_area_libraries
from notifications_utils.template import BroadcastPreviewTemplate
from orderedset import OrderedSet
from app.models import JSONModel
from app.models import JSONModel, ModelList
from app.notify_client.broadcast_message_api_client import (
broadcast_message_api_client,
)
@@ -24,7 +24,6 @@ class BroadcastMessage(JSONModel):
'personalisation',
'starts_at',
'finishes_at',
'status',
'created_at',
'approved_at',
'cancelled_at',
@@ -57,6 +56,12 @@ class BroadcastMessage(JSONModel):
*self._dict['areas']
)
@property
def initial_area_names(self):
return [
area.name for area in self.areas
][:10]
@property
def polygons(self):
return broadcast_area_libraries.get_polygons_for_areas_lat_long(
@@ -72,6 +77,16 @@ class BroadcastMessage(JSONModel):
)
return BroadcastPreviewTemplate(response['data'])
@property
def status(self):
if (
self._dict['status']
and self._dict['status'] == 'broadcasting'
and self.finishes_at < datetime.utcnow().isoformat()
):
return 'completed'
return self._dict['status']
def add_areas(self, *new_areas):
broadcast_message_api_client.update_broadcast_message(
broadcast_message_id=self.id,
@@ -109,3 +124,14 @@ class BroadcastMessage(JSONModel):
broadcast_message_id=self.id,
service_id=self.service_id,
)
class BroadcastMessages(ModelList):
model = BroadcastMessage
client_method = broadcast_message_api_client.get_broadcast_messages
def with_status(self, *statuses):
return [
broadcast for broadcast in self if broadcast.status in statuses
]

View File

@@ -24,6 +24,9 @@ class BroadcastMessageAPIClient(NotifyAdminAPIClient):
return broadcast_message
def get_broadcast_messages(self, service_id):
return self.get(f'/service/{service_id}/broadcast-message')['broadcast_messages']
def get_broadcast_message(self, *, service_id, broadcast_message_id):
return self.get(f'/service/{service_id}/broadcast-message/{broadcast_message_id}')

View File

@@ -1,5 +1,43 @@
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading %}
{% extends "withnav_template.html" %}
{% macro broadcast_table(broadcasts, empty_message) %}
<div class='dashboard-table ajax-block-container'>
{% call(item, row_number) list_table(
broadcasts,
caption="Live broadcasts",
caption_visible=False,
empty_message=empty_message,
field_headings=[
'Template name',
'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="#">{{ item.template_name }}</a>
<span class="file-list-hint-large">
To {{ item.initial_area_names|formatted_list(before_each='', after_each='') }}
</span>
</div>
{% endcall %}
{% call field(align='right') %}
<p class="govuk-body govuk-!-margin-top-6 govuk-!-margin-bottom-0">
{% if item.status == 'broadcasting' %}
Live until {{ item.finishes_at|format_datetime_relative }}
{% elif item.status == 'cancelled' %}
Stopped {{ item.cancelled_at|format_datetime_relative }}
{% else %}
Finished {{ item.finishes_at|format_datetime_relative }}
{% endif %}
</p>
{% endcall %}
{% endcall %}
</div>
{% endmacro %}
{% block service_page_title %}
Dashboard
{% endblock %}
@@ -8,8 +46,12 @@
<h1 class="govuk-visually-hidden">Dashboard</h1>
<p class="govuk-body">
Broadcasts here
</p>
<h2 class="heading-medium govuk-!-margin-bottom-2">Live broadcasts</h2>
{{ broadcast_table(live_broadcasts, 'You do not have any live broadcasts at the moment') }}
<h2 class="heading-medium govuk-!-margin-bottom-2">Previous broadcasts</h2>
{{ broadcast_table(previous_broadcasts, 'You do not have any previous broadcasts') }}
{% endblock %}

View File

@@ -635,3 +635,44 @@ def assert_url_expected(actual, expected):
def find_element_by_tag_and_partial_text(page, tag, string):
return [e for e in page.find_all(tag) if string in e.text][0]
def broadcast_message_json(
*,
id_,
service_id,
template_id,
status,
created_by_id,
starts_at=None,
finishes_at=None,
cancelled_at=None,
):
return {
'id': id_,
'service_id': service_id,
'template_id': template_id,
'template_version': 123,
'template_name': 'Example template',
'personalisation': {},
'areas': [
'england', 'scotland',
],
'status': status,
'starts_at': starts_at,
'finishes_at': finishes_at,
'created_at': None,
'approved_at': None,
'cancelled_at': cancelled_at,
'updated_at': None,
'created_by_id': created_by_id,
'approved_by_id': None,
'cancelled_by_id': None,
}

View File

@@ -46,15 +46,46 @@ def test_dashboard_redirects_to_broadcast_dashboard(
),
def test_empty_broadcast_dashboard(
client_request,
service_one,
mock_get_no_broadcast_messages,
):
service_one['permissions'] += ['broadcast']
page = client_request.get(
'.broadcast_dashboard',
service_id=SERVICE_ONE_ID,
)
assert [
normalize_spaces(row.text) for row in page.select('tbody tr .table-empty-message')
] == [
'You do not have any live broadcasts at the moment',
'You do not have any previous broadcasts',
]
@freeze_time('2020-02-20 02:20')
def test_broadcast_dashboard(
client_request,
service_one,
mock_get_broadcast_messages,
):
service_one['permissions'] += ['broadcast']
client_request.get(
page = client_request.get(
'.broadcast_dashboard',
service_id=SERVICE_ONE_ID,
),
)
assert [
normalize_spaces(row.text) for row in page.select('table')[0].select('tbody tr')
] == [
'Example template To England and Scotland Live until tomorrow at 2:20am',
]
assert [
normalize_spaces(row.text) for row in page.select('table')[1].select('tbody tr')
] == [
'Example template To England and Scotland Finished yesterday at 8:20pm',
'Example template To England and Scotland Stopped 10 February at 2:20am',
]
def test_broadcast_page(

View File

@@ -2,6 +2,7 @@ import json
import os
from contextlib import contextmanager
from datetime import date, datetime, timedelta
from functools import partial
from unittest.mock import Mock, PropertyMock
from uuid import UUID, uuid4
@@ -17,6 +18,7 @@ from . import (
TestClient,
api_key_json,
assert_url_expected,
broadcast_message_json,
generate_uuid,
invite_json,
job_json,
@@ -4175,34 +4177,13 @@ def mock_get_draft_broadcast_message(
def _get(
*, service_id, broadcast_message_id
):
return {
'id': broadcast_message_id,
'service_id': service_id,
'template_id': fake_uuid,
'template_version': 123,
'template_name': 'Example template',
'personalisation': {},
'areas': [
'england', 'scotland',
],
'status': 'draft',
'starts_at': None,
'finishes_at': None,
'created_at': None,
'approved_at': None,
'cancelled_at': None,
'updated_at': None,
'created_by_id': fake_uuid,
'approved_by_id': None,
'cancelled_by_id': None,
}
return broadcast_message_json(
id_=broadcast_message_id,
service_id=service_id,
template_id=fake_uuid,
status='draft',
created_by_id=fake_uuid,
)
return mocker.patch(
'app.broadcast_message_api_client.get_broadcast_message',
@@ -4210,6 +4191,80 @@ def mock_get_draft_broadcast_message(
)
@pytest.fixture(scope='function')
def mock_get_no_broadcast_messages(
mocker,
fake_uuid,
):
return mocker.patch(
'app.models.broadcast_message.BroadcastMessages.client_method',
return_value=[
broadcast_message_json(
id_=fake_uuid,
service_id=SERVICE_ONE_ID,
template_id=fake_uuid,
status='draft', # draft broadcasts arent shown on the dashboard
created_by_id=fake_uuid,
),
],
)
@pytest.fixture(scope='function')
def mock_get_broadcast_messages(
mocker,
fake_uuid,
):
def _get(service_id):
partial_json = partial(
broadcast_message_json,
id_=fake_uuid,
service_id=service_id,
template_id=fake_uuid,
created_by_id=fake_uuid,
)
return [
partial_json(
status='draft',
),
partial_json(
status='broadcasting',
starts_at=(
datetime.utcnow()
).isoformat(),
finishes_at=(
datetime.utcnow() + timedelta(hours=24)
).isoformat(),
),
partial_json(
status='completed',
starts_at=(
datetime.utcnow() - timedelta(hours=12)
).isoformat(),
finishes_at=(
datetime.utcnow() - timedelta(hours=6)
).isoformat(),
),
partial_json(
status='cancelled',
starts_at=(
datetime.utcnow() - timedelta(days=1)
).isoformat(),
finishes_at=(
datetime.utcnow() - timedelta(days=100)
).isoformat(),
cancelled_at=(
datetime.utcnow() - timedelta(days=10)
).isoformat(),
),
]
return mocker.patch(
'app.models.broadcast_message.BroadcastMessages.client_method',
side_effect=_get,
)
@pytest.fixture(scope='function')
def mock_update_broadcast_message(
mocker,