mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-28 10:13:59 -04:00
Merge pull request #3514 from alphagov/broadcast-dashboard
Broadcast dashboard
This commit is contained in:
@@ -59,7 +59,6 @@
|
||||
padding-right: govuk-spacing(2);
|
||||
background: $light-blue-25;
|
||||
margin-right: govuk-spacing(3);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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}')
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user