diff --git a/app/main/views/broadcast.py b/app/main/views/broadcast.py index 6a795d4b8..0ae193919 100644 --- a/app/main/views/broadcast.py +++ b/app/main/views/broadcast.py @@ -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'), ) diff --git a/app/models/broadcast_message.py b/app/models/broadcast_message.py index 041d3831a..bdc4c37bf 100644 --- a/app/models/broadcast_message.py +++ b/app/models/broadcast_message.py @@ -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 + ] diff --git a/app/notify_client/broadcast_message_api_client.py b/app/notify_client/broadcast_message_api_client.py index 57439798d..2f04a76f5 100644 --- a/app/notify_client/broadcast_message_api_client.py +++ b/app/notify_client/broadcast_message_api_client.py @@ -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}') diff --git a/app/templates/views/broadcast/dashboard.html b/app/templates/views/broadcast/dashboard.html index c92ce3992..64672943e 100644 --- a/app/templates/views/broadcast/dashboard.html +++ b/app/templates/views/broadcast/dashboard.html @@ -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) %} +
+ {% 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() %} +
+ {{ item.template_name }} + + To {{ item.initial_area_names|formatted_list(before_each='', after_each='') }} + +
+ {% endcall %} + {% call field(align='right') %} +

+ {% 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 %} +

+ {% endcall %} + {% endcall %} +
+{% endmacro %} + {% block service_page_title %} Dashboard {% endblock %} @@ -8,8 +46,12 @@

Dashboard

-

- Broadcasts here -

+

Live broadcasts

+ + {{ broadcast_table(live_broadcasts, 'You do not have any live broadcasts at the moment') }} + +

Previous broadcasts

+ + {{ broadcast_table(previous_broadcasts, 'You do not have any previous broadcasts') }} {% endblock %} diff --git a/tests/__init__.py b/tests/__init__.py index 14ebdd8ac..a40715e9e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -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, + } diff --git a/tests/app/main/views/test_broadcast.py b/tests/app/main/views/test_broadcast.py index 984a3fd6e..13a933f2e 100644 --- a/tests/app/main/views/test_broadcast.py +++ b/tests/app/main/views/test_broadcast.py @@ -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( diff --git a/tests/conftest.py b/tests/conftest.py index 81f54d481..6cee5a58b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 aren’t 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,